Hello World
Spiga

总结一下F#中运算符的定义规则

2009-12-14 01:46 by 老赵, 5276 visits

F#允许开发人员定义或重载各类运算符,合理利用这一规则可以让编程变得方便,让代码更容易阅读。例如,在使用F#的MailboxProcessor的时候,我会习惯于定义一个运算符来代替显式的Post操作:

let (>>) m (agent: MailboxProcessor<_>) = agent.Post m

这样便可以这样发送消息:

let agent = MailboxProcessor.Start(fun o -> async { o |> ignore });
"hello world" >> agent

不过,F#的运算符定义规则较为复杂,要搞清楚编译器的整体处理方式着实花费了一番功夫。比较奇怪的是,即便是《Expert F#》中对于这个话题也没有详细的叙述——更夸张的是MSDN的文档也相当马虎,甚至有代码缺失以及与试验不符情况(因为还没有正式发布?)。于是我连看带试,最终打算总结一番,作为备忘的同时也算是补充互联网资源。

运算符重载

F#中允许在global级别重载一个运算符,甚至“覆盖”原有的定义。例如,我们可以写一个Operator模块,其中只有一个“加号”的定义:

// operator.fs
#light

module Operator

let (+) (a:int) (b:int) = a * b

我们可以在另一个模块中引入Operator模块,于是两个整数的“加法”便可以得出乘法的效果了:

1 + 2 |> printfn "%i" // 2

从中也可以看出,胡乱重载运算符实在是一种没事找事的方式。因此,现在这篇文章纯粹都是在“谈技术”,所有的内容,包括示例都代表“最佳实践”。

运算符的组成

在F#中,自定义运算符可以由以下字符组成:

! % & * + - . / < = > ? @ ^ | ~

目前在MSDN中,《Operator Overloading (F#)》一文写到“$”也可以作为运算符的组成,不过最新的F#编译器(v1.9.7.4)中会对此作出“警告”,表示以后它将成为一个F#的保留字,不允许用作运算符。

在F#中,每个运算符不限长度。也就是说,如果您喜欢的话,完全可以定义这样的一个运算符来表示整数乘法:

let (!%&*+-./<=>?@^|~!%&*+-./<=>?@^|~) (x:int) (y:int) = x * y

F#会将运算符编译为程序集中具体的方法,其命名遵循一定规则。不过在使用时我们并不需要关心这些。如果您对这方面的具体信息感兴趣,可以参考MSDN中《Operator Overloading (F#)》一文。

前缀与中缀运算符

前缀(prefix)运算符,表示运算符出现在运算数之前,例如“负号”便是个前缀运算符:

let a = 1
-a |> printfn "%i" // -1

中缀(postfix)运算符,表示运算符出现在两个运算数之间,例如最常见的“加法”便是个中缀运算符:

1 + 2 |> printfn "%i" // 3

在自定义运算符时,F#并不允许我们指定某个运算符是前缀还是中缀运算符,编译器会自动根据运算符的“首字母”来决定它是前缀还是中缀的。例如,首字母为“感叹号”的运算符便是“前缀”运算符:

let (!+) (x:int) (y:int) = x + y

根据这个规则,我们只能将“!+”作为前缀运算符来使用:

1 (!+) 2 |> printfn "%i" // 编译失败!
!+ (!+ 1 2) 3 |> printfn "%i" // 6

关于某个字母表示前缀还是中缀运算符,您可以参考《Operator Overloading (F#)》一文中的表格。可以发现,大部分运算符都是中缀的,而只有少数是前缀运算符。至于后缀运算符……F#并不支持后缀运算符。

运算符的优先级

每个运算符有其自己的优先级(precedence),优先级表示一个表达式中连续出现多个运算符时,究竟哪个运算符先生效。例如,我们都知道“先乘除后加减”:

3 + 4 * 5 |> printfn "%i" // 23

那么,我们自定义的运算符优先级又如何呢?F#同样是通过运算符的首字母来决定它的优先级的,关于不同首字母的优先级高低,可以参考MSDN中《Symbol and Operator Reference (F#)》的Operator Precedence一节,它按照优先级从低到高列举所有的运算符。

例如“除号”的优先级比“加号”高,因此:

let (+/) (x:int) (y:int) = x / y
let (/+) (x:int) (y:int) = x + y

4 + 4 / 2 |> printfn "%i" // 6
4 /+ 4 +/ 2 |> printfn "%i" // 4

值得注意的是,如果两个运算符的首字母相同,则F#便认为两个运算符的优先级相同,而不在比较它们后续字符的优先级高低。不过在优先级的判定中有个特例,那就是“点”,它并不参与优先级的比较中,此时便以后面的字符为准了:

let (.+) (x:int) (y:int) = x + y
let (..*) (x:int) (y:int) = x * y

// 仍然是“先乘除后加减”
3 .+ 4 ..* 5 |> printfn "%i" // 23
3 ..* 4 .+ 5 |> printfn "%i" // 17

当然,括号可以改变运算符的优先级,这点再正常不过了。还有一点,便是“转发”操作(即本文代码中出现的“|>”),它以“|”作为首字母。根据规则,它的优先级是很低的(在自定义运算符中是最低的)。因此,无论我们左侧的表达式中使用了什么样的运算符,都是最后才进行“转发”操作。

运算符的相关性

每个运算符都有其相关性(associativity)。相关性的作用是,一旦一个表达式中连续出现优先级相同的运算符,那么它们究竟是从左向右计算(左相关),还是从右向左计算(右相关)。

例如,最普通的“除号”便是左相关的:

4 / 2 / 2 |> printfn "%i" // 1

而List操作的“连接符”(连接单个元素与一个列表)便是右相关的:

1 :: 2 :: 3 :: [] |> printfn "%A" // [1; 2; 3]

在F#中,运算符的相关性也是由首字母决定的,您可以在MSDN中《Symbol and Operator Reference (F#)》的Operator Precedence一节查到所有字符的相关性。

例如,“大于号”是左相关的,因此:

let (>+) (x:int) (y:int) = x + y
let (>*) (x:int) (y:int) = x * y

3 >+ 4 >* 5 |> printfn "%i" // 35
3 >* 4 >+ 5 |> printfn "%i" // 17

而“^”是右相关的:

let (^+) (x:int) (y:int) = x + y
let (^*) (x:int) (y:int) = x * y

3 ^+ 4 ^* 5 |> printfn "%i" // 23
3 ^* 4 ^+ 5 |> printfn "%i" // 27

自然,括号可以改变运算符的相关性。

一元运算符

之前我们讨论的大都是二元运算符(即需要两个运算数),不过有一个字符比较特殊,它便是“~”,我们可以利用它来定义一个“一元运算符”:

let (~-) (x:int) = x + 1

let a = 1
-a |> printfn "%i" // 2

这效果是不是很神奇?因此,如果您要重载现有的运算符,请一定三思而后行。

为类型定义运算符

之前我们一直在讨论“全局”级别的运算符。事实上,运算符也可以定义在某个类型内部。例如:

// 定义
type Rational(numer, denom) =

    member r.Numer = numer
    member r.Denom = denom

    static member (-) (x:Rational, y:Rational) =
        let n = x.Numer * y.Denom - y.Numer * x.Denom
        let d = x.Denom * y.Denom
        new Rational(n, d)

    static member (~-) (v:Rational) = new Rational(-v.Numer, v.Denom)

// 使用
let r1 = new Rational(1, 2)
let r2 = new Rational(2, 3)
let r3 = r1 - r2
let r4 = -r1

至于运算符的优先级、相关性等性质,都与上文描述的保持一致。

Creative Commons License

本文基于署名 2.5 中国大陆许可协议发布,欢迎转载,演绎或用于商业目的,但是必须保留本文的署名赵劼(包含链接),具体操作方式可参考此处。如您有任何疑问或者授权方面的协商,请给我留言

Add your comment

24 条回复

  1. 老赵
    admin
    链接

    老赵 2009-12-14 01:47:00

    我打算推广F#鸟,有打算接下来会抄出一本F#的系列文章来,现在F#的资料实在是不多,更别说中文资源了。

  2. Ariex
    *.*.*.*
    链接

    Ariex 2009-12-14 03:29:00

    在F#中,每个运算符不限长度。也就是说,如果您喜欢的话,完全可以定义这样的一个运算符来表示整数加法


    介里应该是乘法吧?改了之后就把我删掉吧~thx~

  3. 摆地摊的
    *.*.*.*
    链接

    摆地摊的 2009-12-14 07:54:00

    @Jeffrey Zhao
    支持老赵的开创精神和分享精神。

  4. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-12-14 08:03:00

    那我就负责推广HLSL了,呵呵。。

  5. 温景良(Jason)
    *.*.*.*
    链接

    温景良(Jason) 2009-12-14 08:26:00

    @Jeffrey Zhao
    老赵快点出啊,一直想学F#,苦于对英文没辙.

  6. oec2003
    *.*.*.*
    链接

    oec2003 2009-12-14 09:08:00

    @Jeffrey Zhao
    期待这个系列快点出来

  7. 老赵
    admin
    链接

    老赵 2009-12-14 09:12:00

    @Ariex
    多谢提醒,删不删倒无所谓啦。

  8. Matr[未注册用户]
    *.*.*.*
    链接

    Matr[未注册用户] 2009-12-14 09:16:00

    装配脑袋:那我就负责推广HLSL了,呵呵。。



    HLSL不知道推广有何意义,就只有Shader用。会的人也不少,更多的人用这个没多大意义 。

  9. 老赵
    admin
    链接

    老赵 2009-12-14 09:54:00

    @Matr
    总有价值的,至少补充了互联网资源啊。

  10. Ivony...
    *.*.*.*
    链接

    Ivony... 2009-12-14 10:03:00

    那我就负责搬板凳学习了。。。

  11. ganok[未注册用户]
    *.*.*.*
    链接

    ganok[未注册用户] 2009-12-14 10:03:00

    如果有一本介绍F#的书,我希望主要讲以下2部分:
    1、介绍F#和C#的异同点,用C#来学习F#的语法。
    2、介绍各种数学表达式在F#中的写法,主要有:矩阵、积分、求导、对数、三角函数等。

  12. 老赵
    admin
    链接

    老赵 2009-12-14 10:17:00

    @ganok
    感觉除了面向对象那部分,用C#来学F#不太好……
    还有数学表达式在F#的写法有必要吗?这些都不是关键吧。

  13. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-12-14 10:46:00

    @Matr
    在DirectX 11出来前,的确没用,但是有了DirectX 11 Compute Shader事情就不一样了。而且我保证那些那HLSL写游戏的文章对大多数人没用,要使用DirectCompute需要从头学起。

  14. Gsanidt
    *.*.*.*
    链接

    Gsanidt 2009-12-14 12:08:00

    我也只有搬板凳学习了…不过希望老赵这个系列能由浅入深,呵呵…

  15. 极品拖拉机
    *.*.*.*
    链接

    极品拖拉机 2009-12-14 12:19:00

    F#有啥特色撒?
    一直没用过

  16. RednaxelaFX
    *.*.*.*
    链接

    RednaxelaFX 2009-12-14 16:20:00

    Ivony...:那我就负责搬板凳学习了。。。


    同上同学习

  17. Rain Shan
    *.*.*.*
    链接

    Rain Shan 2009-12-14 17:42:00

    RednaxelaFX:

    Ivony...:那我就负责搬板凳学习了。。。


    同上同学习


    报名,排队,交钱

    每位10¥

  18. 为了梦想
    *.*.*.*
    链接

    为了梦想 2009-12-14 18:38:00

    先排队交rmb

  19. 算法城管
    *.*.*.*
    链接

    算法城管 2009-12-14 20:58:00

    F# 听说能写出优美的算法。

  20. 老赵
    admin
    链接

    老赵 2009-12-14 22:30:00

    @算法城管
    算法设计还和语言有关啊?

  21. 装配脑袋
    *.*.*.*
    链接

    装配脑袋 2009-12-15 09:00:00

    也许F#的expression tree更适合用来构建显卡编程的并行程序呢。。?

  22. 老赵
    admin
    链接

    老赵 2009-12-15 09:21:00

    @装配脑袋
    这我就不清楚了……

  23. 链接

    begtostudy 2010-08-25 15:59:13

    请教一个问题

    let inline (-) (m:float[]) (n:float[]):float[] =
        [| for i = 0 to Array.length m - 1 do yield m.[i]-n.[i] |]
    

    为啥之后的就会出错

    let y=1.0-0.0
    
    Error    1   This expression was expected to have type     float []     but here has type     float      E:\MyDocuments\Visual Studio 2010\Projects\curve intersection\newton\Module1.fs 31  7   newton
    Error    2   This expression was expected to have type     float []     but here has type     float      E:\MyDocuments\Visual Studio 2010\Projects\curve intersection\newton\Module1.fs 31  11  newton
    
  24. 老赵
    admin
    链接

    老赵 2010-08-25 21:06:28

    @begtostudy

    不是吧,这么明显的错误信息……

发表回复

登录 / 登录并记住我 ,登陆后便可删除或修改已发表的评论 (请注意保留评论内容)

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

评论内容(大于5个字符):

  1. Your Name yyyy-MM-dd HH:mm:ss

使用Live Messenger联系我