Hello World
Spiga

最容易令初学者混乱的F#命令

2010-03-31 12:36 by 老赵, 1730 visits

话说,其实我也是F#的初学者,至少相对于C#等语言来说一定是这样的。而对于初学者,或是C#和F#混用的程序员来说,我认为最F#中最容易令人混乱的命令是Reference Cells的取值操作了。下面便详细谈谈这么说的原因,及建议的应对办法。

F#是一门函数式编程语言,函数式编程语言的特点之一便是No Side Effect(无副作用),Immutable(不可变)。但是在很多场景下,Mutable(可变)可以给我带来很多便利,尤其是在结合命令式编程的场景中。因此F#提供了将某个“标识符”定义为“可变”的方式,主要有两种:使用mutable关键字或是Reference Cells。在大部分情况下,我推荐(微软也这么推荐的)使用mutable关键字,因为这样标识符在使用上也已经和普通变量没有任何区别了。与之相对,使用Reference Cell进行读写操作都需要一些特殊的操作/指令。不过的确有一些场景必须使用Reference Cells,您可以关注MSDN上的说明

例如,在mutable的标识符在读取和赋值时,和普通的属性没有什么区别:

let mutable a = 0
a <- 1 // assign mutable variable

let request = WebRequest.Create("http://blog.zhaojie.me")
request.ContentType <- "text/xml" // assign property

但是对于Reference Cells来说,它的读取和写入就需要使用!与:=操作符了:

let a = ref 0
a := 1 // assign value
printfn "%i" !a // retrieve value

这个感叹号便是引起混乱的源泉,且看以下代码:

let transfer (streamIn: Stream) (streamOut: Stream) buffer =
    let hasData = ref true
    while !hasData do
        let lengthRead = streamIn.Read(buffer, 0, buffer.Length)
        if lengthRead > 0 then 
            streamOut.Write(buffer, 0, lengthRead)
        else
            hasData := false

上面的代码定义了一个transfer函数,将一个数据流中的数据全部传输到另一个数据流中。在这里我们使用了命令式的编程方式,并使用一个名为hasData的Ref Cell来表明是否读完了数据。不过,您看到while语句中的!hasData是什么感觉?至少对于我这样混写C#和F#的人来说,我的第一反应是“嗯,取反?”,然后才是“哦,只是Ref Cells的取值操作”。对于其他一些场景下可能这点不会成为问题,但如果这个Ref Cell是个布尔值,然后又放在if或while的时候,混乱就这样开始了。

因此,我目前可能会倾向于使用这样的方式:

let transfer (streamIn: Stream) (streamOut: Stream) buffer =
    let hasData = ref true
    while hasData.Value do
        let lengthRead = streamIn.Read(buffer, 0, buffer.Length)
        if lengthRead > 0 then 
            streamOut.Write(buffer, 0, lengthRead)
        else
            hasData := false

在F#中,一个Ref Cell其实是一个Ref<'a>类型的对象,它有一个'a类型(泛型类型)的Value属性,可读写。因此,如果我们在上面的代码中直接使用Value属性,那么我想就不会让任何人混乱了。

当然,最好的办法可能还是写一些immutable的代码吧,例如:

let rec transfer (streamIn: Stream) (streamOut: Stream) buffer =
    let lengthRead = streamIn.Read(buffer, 0, buffer.Length)
    if lengthRead > 0 then
        streamOut.Write(buffer, 0, lengthRead)
        transfer streamIn streamOut buffer

对于F#来说,这样的(尾)递归和之前的实现方式可以说是完全等价的。

Creative Commons License

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

Add your comment

21 条回复

  1. doylecnn
    222.68.249.*
    链接

    doylecnn 2010-03-31 14:01:58

    一楼是老赵的仇人?

  2. 老赵
    admin
    链接

    老赵 2010-03-31 15:39:33

    @法克犹

    不好意思啊,您说话带脏字了,俺就删了。

    欢迎下次换个文明的方式来骂人,呵呵。

  3. 链接

    admin.y 2010-03-31 15:48:57

    提问:请问老赵windows7主要是用什么语言开发的呀? O(∩_∩)O~

  4. 老赵
    admin
    链接

    老赵 2010-03-31 16:11:14

    @admin.y 提问:请问老赵windows7主要是用什么语言开发的呀?

    内核主要是C,少部分汇编。其他部分C++用的也很多,然后外围环绕着.NET。

  5. hiessu
    118.202.225.*
    链接

    hiessu 2010-03-31 19:45:35

    老赵是怎么看OCaml与F#的呢?

  6. jason
    221.175.119.*
    链接

    jason 2010-03-31 22:26:40

    还以为老赵有了独立博客,博客园就不更新了呢,今天看了不担心了

  7. 老赵
    admin
    链接

    老赵 2010-03-31 22:34:21

    @hiessu: 老赵是怎么看OCaml与F#的呢?

    基本没替代关系,还有对OCaml也不了解啊。

  8. zizon
    174.37.230.*
    链接

    zizon 2010-03-31 23:57:07

    看rss没全文输出差点退订了. 还好看到了 原文链接 几个字才过来.

  9. 老赵
    admin
    链接

    老赵 2010-04-01 00:24:56

    @zizon 看rss没全文输出差点退订了. 还好看到了 原文链接 几个字才过来.

    直接订阅我现在这个吧。

  10. 麒麟.NET
    124.42.13.*
    链接

    麒麟.NET 2010-04-01 11:06:20

    现在这个订阅有点问题,最新的RSS不是出现在最上面,而是每次都有一大批旧文章出现在最上面。不知道是阅读器的问题还是RSS的问题。我用的是有道阅读器。

  11. 老赵
    admin
    链接

    老赵 2010-04-01 14:31:06

    @麒麟.NET: 现在这个订阅有点问题,最新的RSS不是出现在最上面,而是每次都有一大批旧文章出现在最上面。

    我试了下没问题啊。

  12. Hiber
    116.232.134.*
    链接

    Hiber 2010-04-02 08:47:00

    我觉得这也是计设时的取舍吧,有些东西设计者不希望用户滥用,但又必须提供相应的功能,就设计的相对“复杂、难用”一些,用起来不顺手,滥用自然就会少一些。

    BTW:这个留言功能好象用户体验不是很好哦,那个SEND MY COMMENT!不是直观。

  13. 老赵
    admin
    链接

    老赵 2010-04-02 09:22:17

    @Hiber: 我觉得这也是计设时的取舍吧,有些东西设计者不希望用户滥用,但又必须提供相应的功能,就设计的相对“复杂、难用”一些,用起来不顺手,滥用自然就会少一些。

    我觉得你的想法很有趣,呵呵。

  14. 阿斯多夫
    221.6.15.*
    链接

    阿斯多夫 2010-04-07 17:56:12

    这个评论有点意思,俺也试试

  15. evanmeng
    222.70.178.*
    链接

    evanmeng 2010-04-14 00:03:49

    不是很懂F#,问一下。最后那一段代码里,transfer函数的返回类型是什么?unit? 这个返回类型是F#根据函数定义自己推断出来的吗? 如果代码变成:

    let rec transfer (streamIn: Stream) (streamOut: Stream) buffer =
        let lengthRead = streamIn.Read(buffer, 0, buffer.Length)
        if lengthRead > 0 then
            streamOut.Write(buffer, 0, lengthRead)
            transfer streamIn streamOut buffer
        else
            0
    

    那是不是transfer的返回类型就自动变成了整数?

  16. 老赵
    admin
    链接

    老赵 2010-04-14 09:08:06

    @evanmeng

    对,F#做的完整的类型推断,返回类型应该是int吧,就是那个0。

  17. evanmeng
    222.70.182.*
    链接

    evanmeng 2010-04-14 18:05:12

    @老赵

    这样的话我就有点不明白了,我记得在Haskell这种类型的语言里,有了if就必定要有else(OCaml似乎也是这样,不确定),因为一个函数必定要被evaluate成某个表达式。 在你原先那个函数里,如果lengthRead>=0,那么transfer函数被evaluate成什么呢?还是F#对于所有没有定义的执行路径都返回某个默认值?

  18. 老赵
    admin
    链接

    老赵 2010-04-14 18:13:57

    @evanmeng

    就是unit,或者说就是“没有返回值”,你可以简单理解为C#等语言里的void。如果if分支返回的是unit,那么就可以没有else了。

    是不是前面一个回复是不是让你有些误会了……其实我说返回int,就是那个0,都是指你给出的代码,呵呵。

  19. chenkai
    123.10.32.*
    链接

    chenkai 2010-05-24 13:32:58

    F#这方面资料 找了一圈发现挺少的 只有部分的开发文档 和CSDN上资料.还算比较完整. 赵F#资料一般怎么来源...??有点疑问.

  20. 老赵
    admin
    链接

    老赵 2010-05-24 14:07:24

    @chenkai

    看书吧,Programming F#和Expert F#等等。

  21. DongPad
    218.242.159.*
    链接

    DongPad 2010-08-13 14:13:01

    Reference Cells怎么翻译? 还有老赵能举个mutually referential的例子么?

发表回复

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

昵称:(必填)

邮箱:(必填,仅用于Gavatar

主页:(可选)

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

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

使用Live Messenger联系我