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两种语言之间很有趣的相似性, 有助于编程实践中彼此借鉴。