F#语言的类型扩展实现面向对象编程 及与GO语言的比较

F#语言作为.NET家族的重要成员, 在基本保持ML系语言OCAML风格和模式的前提下, 又加入诸如C#在面向对象方面的许多内容, 使之成为既有函数式(为主)又有OOP的多模式编程语言。其OOP方面的语法高度秉承了C#关于类和对象的语言特征与风格。

F# 做 OOP 的标准套路:

module asClass =
  type ICounter = interface
    abstract GetState: unit -> int
    abstract Increment: int -> unit
  end
  type CounterClass (init) =
    let mutable count = init
    interface ICounter with
      member this.GetState () = count
      member this.Increment step =
        count <- count + step
    member this.GetState () = (this :> ICounter).GetState()
    member this.Increment step = (this :> ICounter).Increment step
    member this.PrintMe () =
      printfn "%s" (this.ToString())

    new() = CounterClass(0)

  let counter = CounterClass()
  counter.Increment 2
  printfn "counter.GetState = %d" (counter.GetState())
  counter.PrintMe()

今天看到有个讨论——record vs。class?,思路大开:
F# 作为以函数式为主的语言,对待函数 跟对待普通值 是同等的 (函数是一等公民嘛), 那么record类型也就能把字段申明成函数:

type ICounter = {
  GetState : unit -> int
  Increment : int -> unit
}

record类型在这里充当了interface。

借助类型扩展, 可以将函数附加到类上, 起到类的行为的作用。这就是给类型添加成员member:

type ICounter with
  member this.PrintMe () =
    printfn "%s" (this.ToString())

而 let申明则"实现"了这个interface:

let counter: ICounter =
  let count = ref 0
  { GetState = (fun () -> !count)
    Increment = (fun step -> count:= !count + step)
  }

调用如下:

counter.Increment 1
printfn "counter.GetState() = %d" (counter.GetState())
counter.PrintMe()

再借助module和private等关键词,就可以达到OOP需要的封装效果。

module Counter =
    let count = ref 0
    let Create(init) =
      count:= init
      { GetState = (fun () -> !count)
        Increment = (fun () -> incr count)
      }

  let counter = Counter.Create(1)
  counter.Increment()
  printfn "counter.GetState() = %d" (counter.GetState())

这里, module充当了OOP的class。

类型扩展可以定义任何成员类型, 如值。而不仅仅是函数。这就好比:在实现和扩展接口时不仅仅限于其中只能是方法。

let绑定相同类型的多个变量,可以赋予他们的字段以不同的函数,不就是多态了嘛!

let counter2 =
  { counter with
      GetState = counter.ToString >> String.length
  }

let CounterAction (c: ICounter) =
  printfn "the c.GetState() = %d" (c.GetState())
  c.PrintMe()

调用如下:

CounterAction counter
CounterAction counter2

用记录代替class 有很多好处, 比如能够运用 pattern matching, type inference, higher order functions, generic equality…等高级功能。
这些都是 class 无法做到的。

F#现有的有关类class的一整套表示方法 绝大多数是从C#搬运过来的,掺杂了太多语法和语义上的噪音,还与F#的优雅风格格格不入,真是强行拉郎配。

这些实现手法突然使我想到了GO语言的接口:

如果有两个类型实现的方法 恰好涵盖了某个interface里所申明的全部方法,那么这两个类型可以视作(自动地)属于同一个类class,并且可以传递给 以该interface为类型type的函数的参数,从而使得该函数具备了多态的特质。虽然GO语言并没有class的语法。也就是说 接口就是两个type的共同父类。

对照前面F#的表现方法,ICounter相当于GO的interface,当counter1和counter2都具有相同的字段,并且同样的函数签名,那么它俩就被F#的类型推导认作是同一个类ICounter,同样可以传给以ICounter为类型的函数的参数,从而使得该函数具备多态功能。

当然,F#也可以显式地申明counter1和counter2同属于ICounter类,或者说都实现了该接口。前面例子就说明了这点。这种显式申明就跟普通类型声明一样形式。这样的显式对于静态强类型语言是很有必要的。

相反,GO的隐式归类会导致隐形bug,增加编程的复杂性, 调试追踪的难度和运行的不确定性, 后果便是程序的不安全性。

有人把这种"隐式归类"吹捧为"轻耦合"。这里我不做深入评判,反正不喜欢约束是大多数人的倾向,就像无类型和弱类型语言总比有类型和强类型语言要更受欢迎 一样道理。每次都要申明一个对象是继承或实现自某个类,就如同每次都要把变量全都申明是某个类型 一样讨人厌。

不过,F#也不是傻乎乎一味要求逢类型必写。它极其聪明的类型推断能使大部分的类型声明可以省略。但这种省略是基于 能确保显式声明的有和无 不会有语义的差别 这个前提下,绝非GO之类靠"巧合"。

长得像鸭子并不一定是鸭子,不安全性由此导致!

F#的类型推导也有"鸭子"原理。但一旦发现有两个类型都有点像 又不全像鸭子,或者有个类型又像鸭子又像鸡,它绝不会自作主张断定它就是鸭子,更不会认为它既是鸡又是鸭。

一旦类型推断发现"两者似乎皆有可能",就要求你改为显式声明,至少有明确的默认首选规则 (比如以最近一个为准)。不允许存在模棱两可。

就拿 “若有某类型实现了有相同方法集合的两个接口,则这个类型同时属于这两个接口” 这点来说,F#是不允许的,你必须明确到底属于哪个接口。正如有专家调侃到:不能因为某类型有个draw方法,就说它既是figue接口 又可以是gun接口!

F#和GO都同样使用结构体(F#的record和GO的Structs)对属性进行封装,但不限于结构体, 也可以其它类型。

正因为F#与 GO 在 OOP 方面有高度相似性, 下面就来一些对比。我就偷懒, 引用GO语言中文网 《Go系列教程》 中的例子, 将部分 GO 代码转化成 F#。每摘录一段 GO 的代码, 后面就跟转化后的 F# 代码。为便于对比, 尽量保持与原有GO所用标识符相同。此外, 尽量保持与原有GO执行逻辑的相同, 未针对F#作专有转化或优化。所有与GO明显不同的写法 都是由于F#必须采用的表示方法。如此一来也可让读者学习和体会F#的别具一格的风格。

代码的解释,原教程里都有,这里就不再重复,以突出代码的对比。

既然是以F#作比较, 当然就应该尽量原汁原味地用F#的原生特性而少用甚至不用C#夹带进来的特性。

  • example 1: 接口的声明与实现

GO:

//interface definition
type VowelsFinder interface {
  FindVowels() []rune
}
type MyString string

//MyString implements VowelsFinder
func (ms MyString) FindVowels() []rune {
  var vowels []rune
  for _, rune := range ms {
    if rune == 'a' || rune == 'e' || rune == 'i' || rune == 'o' || rune == 'u' {
      vowels = append(vowels, rune)
    }
  }
  return vowels
}
func main() {
  name := MyString("Sam Anderson")
  var vowels VowelsFinder
  vowels = name // possible since MyString implements VowelsFinder
  fmt.Printf("Vowels in %s are %c", name, vowels.FindVowels())
}

F#:

type VowelsFinder<'any> = {
      FindVowels: unit -> 'any list
    }
  type MyString = s of string
  type MyString with
    member ms.FindVowels () =
      let isVowel letter =
        match letter with
        | 'a'| 'e'| 'i'| 'o'| 'u' -> true
        |_ -> false
      (string ms) |> Seq.toList |> (List.filter isVowel)

  let name = MyString.s "Sam Anderson"
  let vowels = {FindVowels = name.FindVowels}
  printfn "Vowels in %s are %A" (string name) (vowels.FindVowels()) // ['a'; 'e'; 'o']
  • example 2: 接口的实际用途, 尤其是体现多态

GO:

type SalaryCalculator interface {
  CalculateSalary() int
}
type Permanent struct {
  empId    int
  basicpay int
  pf       int
}
type Contract struct {
  empId    int
  basicpay int
}

//salary of permanent employee is sum of basic pay and pf
func (p Permanent) CalculateSalary() int {
  return p.basicpay + p.pf
}

//salary of contract employee is the basic pay alone
func (c Contract) CalculateSalary() int {
  return c.basicpay
}

/*
total expense is calculated by iterating though the SalaryCalculator slice and summing
the salaries of the individual employees
*/
func totalExpense(s []SalaryCalculator) {
  expense := 0
  for _, v := range s {
    expense = expense + v.CalculateSalary()
  }
  fmt.Printf("Total Expense Per Month $%d", expense)
}
func main() {
  pemp1 := Permanent{1, 5000, 20}
  pemp2 := Permanent{2, 6000, 30}
  cemp1 := Contract{3, 3000}
  employees := []SalaryCalculator{pemp1, pemp2, cemp1}
  totalExpense(employees)
}

F#:

type SalaryCalculator = {
    CalculateSalary: unit -> int
  }
  type Permanent = {
    empID: int
    basicPay: int
    pf: int
  }
  type Contract = {
    empID: int
    basicPay: int
  }
  type Permanent with
    member p.CalculateSalary () =
      p.basicPay + p.pf
  type Contract with
    member c.CalculateSalary () =
      c.basicPay

  let totalExpense (s: SalaryCalculator list) =
    let expense = List.sumBy (fun x -> x.CalculateSalary()) s
    printfn "Total Expense Per Month $%d" expense

  let p1 = {empID = 1; basicPay = 5000; pf = 20}
  let p2 = {empID = 2; basicPay = 6000; pf = 30}
  let c1 = {empID = 3; basicPay = 3000}

F#要求显式申明哪些类型"实现"了这个接口. 这里模拟了GO的语义–方法全对得上是"实现"的判断标准

  let pemp1 = {CalculateSalary = p1.CalculateSalary}
  let pemp2 = {CalculateSalary = p2.CalculateSalary}
  let cemp1 = {CalculateSalary = c1.CalculateSalary}
  let employees = [pemp1; pemp2; cemp1]
  totalExpense employees // $14050
  • example 3: 空接口, 类型断言

GO:

func assert(i interface{}) {
  s := i.(int) //get the underlying int value from i
  fmt.Println(s)
}
func main() {
  var i interface{} = 56
  assert(i)
  var s interface{} = "Steven Paul"
  assert(s)
}

F#:

type Interface<'any> = Empty of 'any
  let Assert i =
    let v =
      match i with
      | Interface.Empty n -> n
    printfn "%A" v

  let i = Interface.Empty 56
  Assert i

  let s = Interface.Empty "Steven Paul"
  Assert s

注意:interface和assert是F#的关键词。
由于 Interface 是真正的泛型, 传给它不同的类型 是不会像GO那样报错的, 不需要动用返回错误标记。

  • example 4: 类型选择(Type Switch)

GO:

func findType(i interface{}) {
  switch i.(type) {
  case string:
    fmt.Printf("I am a string and my value is %s\n", i.(string))
  case int:
    fmt.Printf("I am an int and my value is %d\n", i.(int))
  default:
    fmt.Printf("Unknown type\n")
  }
}
func main() {
  findType("Naveen")
  findType(77)
  findType(89.98)
}

F#:

type Describer = {
    Describe: unit -> unit
  }
  type Person = {
    name: string
    age: int
  }
  type Person with
    member p.Describe () =
      printfn "%s is %d years old" p.name p.age

  let (|Describer|Others|) x =
    match box x with
    | :? Person as p -> Describer p
    | _ -> Others x

  let findType i =
    match i with
    | Describer i -> i.Describe()
    | Others i -> printfn "the type of %A is %A" i (i.GetType())

  findType "Susan"
  let p = {name = "Naveen R"; age = 25}
  findType p
  • example 5: 实现多个接口

GO:

type SalaryCalculator interface {
  DisplaySalary()
}
type LeaveCalculator interface {
  CalculateLeavesLeft() int
}
type Employee struct {
  firstName   string
  lastName    string
  basicPay    int
  pf          int
  totalLeaves int
  leavesTaken int
}

func (e Employee) DisplaySalary() {
  fmt.Printf("%s %s has salary $%d", e.firstName, e.lastName, (e.basicPay + e.pf))
}
func (e Employee) CalculateLeavesLeft() int {
  return e.totalLeaves - e.leavesTaken
}
func main() {
  e := Employee{
    firstName:   "Naveen",
    lastName:    "Ramanathan",
    basicPay:    5000,
    pf:          200,
    totalLeaves: 30,
    leavesTaken: 5,
  }
  var s SalaryCalculator = e
  s.DisplaySalary()
  var l LeaveCalculator = e
  fmt.Println("\nLeaves left =", l.CalculateLeavesLeft())
}

F#:

type SalaryCalculator = {
    DisplaySalary: unit -> unit
  }
  type LeaveCalculator = {
    CalculateLeavesLeft: unit -> int
  }
  type Employee = {
    name: string
    basicPay: int
    pf: int
    totalLeaves: int
    leaveTaken: int
  }
  type Employee with
    member e.DisplaySalary () =
      printf "%s has salary $%d" e.name (e.basicPay + e.pf)
    member e.CalculateLeavesLeft () =
      e.totalLeaves - e.leaveTaken

  let e = {
    name = "Naveen Ramanathan"
    basicPay = 5000
    pf = 200
    totalLeaves = 30
    leaveTaken = 5
  }

  let s = {DisplaySalary = e.DisplaySalary}
  s.DisplaySalary ()

  let l = {CalculateLeavesLeft = e.CalculateLeavesLeft}
  printfn "\nLeaves left = %d" (l.CalculateLeavesLeft())
  • example 6: 接口的嵌套

GO:

type SalaryCalculator interface {
  DisplaySalary()
}
type LeaveCalculator interface {
  CalculateLeavesLeft() int
}
type EmployeeOperations interface {
  SalaryCalculator
  LeaveCalculator
}
type Employee struct {
  firstName   string
  lastName    string
  basicPay    int
  pf          int
  totalLeaves int
  leavesTaken int
}

func (e Employee) DisplaySalary() {
  fmt.Printf("%s %s has salary $%d", e.firstName, e.lastName, (e.basicPay + e.pf))
}
func (e Employee) CalculateLeavesLeft() int {
  return e.totalLeaves - e.leavesTaken
}
func main() {
  e := Employee{
    firstName:   "Naveen",
    lastName:    "Ramanathan",
    basicPay:    5000,
    pf:          200,
    totalLeaves: 30,
    leavesTaken: 5,
  }
  var empOp EmployeeOperations = e
  empOp.DisplaySalary()
  fmt.Println("\nLeaves left =", empOp.CalculateLeavesLeft())
}

F#:

  /// type SalaryCalculator, type LeaveCalculator, type Employee, let e 与上例相同, 不再重复.
  type EmployeeOperations = SalaryCalculator * LeaveCalculator
  let empOp =
    {DisplaySalary = e.DisplaySalary},
    {CalculateLeavesLeft = e.CalculateLeavesLeft}
  let s, l = empOp
  s.DisplaySalary()
  printfn "\nLeaves left = %d" (l.CalculateLeavesLeft())

看来,F#也可以通过类似组合的方式而实现OOP的继承的作用,虽然没有GO的匿名结构嵌入那样的方便。期待未来能引入 typeScript 的交叉类型那样 将几个记录类型的字段平铺到自己类型里来。

本次话题尚未涉及封装。其实, 如果在同一个模块中, 对类成员的可见性控制, GO与F#是基本雷同的, 只差在GO采用了极简的首字母大小写的语法。

通过以上比较, 可以了解F#与G0两种语言之间很有趣的相似性, 有助于编程实践中彼此借鉴。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值