F#系列随笔索引页面

在本系列的第二部分(函数式编程上、中、下)中,我们了解了如何使用F#进行纯粹的函数式编程。但是在一些情况下,比如I/O,几乎不能避免改变状态,也就是说会带来side effect。F#并不强求你以无状态的方式编写程序,它提供了可修改(mutable)的标识符来解决这类问题,同时它还提供了其它的程序结构以支持命令式编程。现在就来对这些特性探个究竟。

首先是unit类型,这种类型表示“没有值”。然后是F#如何处理可修改的值。最后来看看如何在F#中使用.NET类库,包括如何调用静态方法、创建对象并使用其成员、使用类的索引器和事件以及F#中的|>操作符。

unit类型

没有参数和返回值的函数的类型为unit,它类似于C#中的void,或者说CLR中的System.Void。对于使用函数式编程语言的开发人员来说,不接受参数、无返回值的函数没多大意义。而在命令式编程中,由于side effect的存在,这样的函数仍有意义。unit类型表示为一对括号“()”。

F# Codelet main() =
()

在这个例子中,main函数的类型为unit -> unit,也就是说它既不接受参数,也无返回值。第一对括号使得main成为一个函数而不是一个简单的值。第二对括号告诉编译器,main函数什么值也不返回。

注意:仅仅将函数命名为main不表示它就是程序的入口,它不会自动执行(像C#的Main方法那样),要执行它,需要在源文件的结尾处调用:main(),在后面的文章中将介绍如何指定F#程序的入口。

再来看看如何调用unit类型的函数,有两种方式:

F# Codelet () = main()// -- or -- main()

我们还可以在函数内部连续多次调用unit类型的函数——只要保证它们的缩进是一样的即可:

F# Codelet poem() =
print_endline "I sent thee late a rosy wreath"
print_endline "Not so much honouring thee"
print_endline "As giving it a hope that there"
print_endline "It could not withered be"

poem()

这是《Song to Celia》中的四句诗词,print_endline函数的类型为string -> unit,我们知道函数的返回值为其函数体内最后一步运算的值,所以poem的类型为unit -> unit。

并不是说只有unit类型的函数可以这么用,但是调用非unit类型的函数时编译器会报告一个警告,因为它可能会有副作用(side effect),比如:warning FS0020: This expression should have type 'unit', but has type 'string',我们看到警告总会感觉不舒服。F#提供了一些机制可将这些警告消除,即将函数转换为unit类型的函数。事实上,这种需求在使用F#库时不多,在使用由其它语言编写的.NET类库时会更多些。看下面的例子:

F# Code#lightlet getString() = "foo bar"

let _ = getString()// -- or --ignore(getString())// -- or --getString() |> ignore

首先是函数getString的定义,下面的三行则是将其转换为unit类型函数的三种方式。第一种是使用下划线“_”,它在前面已经出现过几次,它一般表示我们对某些值不感兴趣;既然不感兴趣,那就可以忽略(ignore)了,这就是第二种方式:ignore函数;第三种方式使用了“|>”操作符,它本质上同第二种一样。“|>”操作符会在稍后介绍。

mutable关键字 

在探险之旅(二)中我们知道可以使用let关键字将值绑定至标识符,在某些情况下,我们还可以重定义(redefine)标识符或者绑定(rebind)新的值,但是不能直接修改它的值。显然,对于我们这些用惯了命令式编程语言的人来说,这实在有些不爽,因为这些语言以修改变量的值作为最基本的运算方式。既然F#也支持命令式编程范式,它当然也能让你修改标识符的值。这就是mutable关键字和“<-”操作符,“<-”的类型为unit(操作符也是函数),下面的例子对此作了演示:

F# Codelet mutable phrase = "Good good study, "
print_endline phrase
phrase <- "day day up."
print_endline phrase

运行结果为:

OutputGood good study,
day day up.

这看起来像是重定义标识符,其实不然。修改标识符时,只能修改它的值而不能修改类型;重定义时可同时改变类型和值(这本质上是定义了一个新的标识符)。事实上,如果你尝试修改标识符的类型,编译器会给你一个错误。另外,它们还有一个重要的差别,即它们的可见性(或者说修改行为的作用域)。在重定义标识符的时候,修改仅仅在新标识符的作用域内有效,一旦出了这个作用域,它就会回复到旧有的值;对于可修改的标识符来说,任何修改都是永久性,与作用域无关。

F# Codelet redefineX() =let x = "One"
printfn "Redefining: \r\nx = %s" xif true thenlet x = "Two"
printfn "x = %s" xelse ()
printfn "x = %s" x

let mutableX() =let mutable x = "One"
printfn "Mutating: \r\nx = %s" xif true then
x <- "Two"
printfn "x = %s" xelse ()
printfn "x = %s" x

redefineX()
mutableX()

运行结果为:

OutputRedefining:
X = One
X = Two
X = One
Mutating:
X = One
X = Two
X = Two

可修改的标识符也有其局限性,在子函数内不能修改它的值。而这也是ref类型的来由,稍后你会看到。

定义可修改的记录(Record)类型 

默认情况下,记录类型是不可变的。不过F#提供了一种特殊的语法,使得我们可以修改记录类型的字段值,即在字段前使用mutable关键字。需要注意的是,这种操作改变的是记录字段的内容而不是记录本身

F# Codetype Couple =
{her : string; mutable him : string}let couple = {her = "Elizabeth Taylor"; him = "Nicky Hilton"}

let print o = printf "%A \r\n" o

let changeCouple() =
print couple;
couple.him <- "Michael Wilding";
print couple;
couple.him <- "Michael Todd";
print couple;

changeCouple()

通过Couple类型的定义可知,him字段是可修改的,就像changeCouple中的代码,但如果尝试修改her的值就会遭遇编译错误。

ref类型

ref类型是一种状态进行修改的简单方式。ref类型其实是包含一个可修改字段的record类型,它定义在F#库中,伴随它的还有两个操作符,它们使得操作ref类型更为方便:

F# Codelet ref x = { contents = x }let (:=) x y = x.contents <- ylet (!) x = x.contents

ref“函数”将输入的值“装入”一个记录类型,同时用“:=”操作符来进行赋值,“!”来取值。进一步分析,ref“函数”的类型为a’ -> Ref<a’>,可以了解到“装入”的记录类型为Ref<a’>,由此可知使用了类型参数化(type parameterization),这个概念前面部分已经介绍过了。这意味着ref可以接受任意类型的值,但是一经赋值,其类型也就固定了。Ref<a’>类型暴露了Value属性,我们也可以通过它来获取或设置ref类型的值。

F# Codelet phrase = ref "Inconsistency"

考虑一个简单的问题,求一个整型数组所有元素的和。先看C#怎么做:

C# Codestatic int TotalArray(int[] array)
{int total = 0;foreach (int element in array)
{
total += element;
}

return total;
}

再看看F#的版本:

F# Codelet totalArray (intArray : int array) =let x = ref 0for n in intArray do
x := !x + n
!x

可以看到F#的命令式编程范式下与C#何其相似!

数组(Array) 

数组算得上是我们最熟悉的数据结构了。F#中的数组基于BCL中的System.Array类型,是一种可修改的集合类型。数组与列表相对,数组中的值是可修改的,而列表中的值则不能;列表的容量(长度)可以动态增大,数组则不能。一维数组又时被称为向量(Vector),多维数组有时被称为矩阵(Matrix)。定义数组时,将各项置于“[|”和“|]”中,各项间用“;”隔开。

下面的例子演示了如何对数组进行读取和写入操作。

F# Code// 定义let rhymeArray = [| "Hello"; "F#" |]

// 读取  let firstPiggy = rhymeArray.[0]let secondPiggy = rhymeArray.[1]

// 写入rhymeArray.[0] <- "Byebye"
rhymeArray.[1] <- "my friend"

// 输出print_endline firstPiggy
print_endline secondPiggy
print_any rhymeArray

数组跟列表一样,也采用了类型参数化,数组的类型为其元素的类型,因此rhymeArray的类型为string array,也可写作string[]。

F#中的多维数组可分为两类:交错数组(jagged array)和规则数组。交错数组,表示数组的数组,就是说最外部数组的元素也是数组(称为内部数组),内部数组的长度不必相同。而规则数组,事实上整个数组作为一个对象,其内部数组的长度是相同的。

先来看看交错数组的用法:

F# Codelet jaggedArray = [| [| "one" |]; [| "two"; "three" |] |]let singleDimension = jaggedArray.[0]let itemOne = singleDimension.[0]let itemTwo = jaggedArray.[1].[0]

printfn "%s %s" itemOne itemTwo // one two

jaggedArray的类型为string array array,这也是为什么说它是数组的数组,操作规则数组的语法有所不同:

F# Codelet square = Array2.create 2 2 0
square.[0,0] <- 1
square.[0,1] <- 2
square.[1,0] <- 3
square.[1,1] <- 4
printf "%A \r\n" square // [| [|1; 2|]; [|3; 4|] |]

square的类型为int[,]。

注意:要编写.NET 1.1 和.NET 2.0兼容的代码,需要使用Microsoft.FSharp.Compatibility命名空间的CompatArray和CompatMatrix类。

数组推导(Array Comprehension) 

前面介绍过了关于列表和序列的推导语法。我们也可以使用类似的语法进行数组推导。

F# Codelet chars = [|'1' .. '9'|]

let squares =
[| for x in 1 .. 9-> x, x * x |]
printfn "%A" chars
printfn "%A" squares

注意:本文中的代码均在F# 1.9.4.17版本下编写,在F# CTP 1.9.6.0版本下可能不能通过编译。

F#系列随笔索引页面

参考:
《Foundations of F#》 by Robert Pickering
《Expert F#》 by Don Syme , Adam Granicz , Antonio Cisternino
《F# Specs》

本文转自一个程序员的自省博客园博客,原文链接:http://www.cnblogs.com/anderslly/archive/2008/09/25/fs-adventure-ip-part-one.html,如需转载请自行联系原作者。

F#探险之旅(三):命令式编程(上)相关推荐

  1. F#探险之旅(四):面向对象编程(中)

    F#系列随笔索引 对象表达式(Object Expressions) F#中的OOP语法很简洁,而对象表达式则正是这种简洁性的核心所在.通过对象表达式,我们可以创建抽象类或接口的轻量级实现,也可以对一 ...

  2. 从高考到程序员:我的程序探险之旅

    就在今天下午,湖南省教育考试院公布了 2017 年湖南省普通高等学校招生全国统一考试的卷面成绩,我的微信也瞬间被各种分段统计表和喜报刷屏,每年的这个时候总是几家欢喜几家愁.六年前的 6 月 25 日, ...

  3. 一篇文章看懂函数式编程与命令式编程

    文章目录 1 历史来源 2 编程范式 3 函数式编程的崛起 4 函数式编程 4.1 函数 4.2 纯函数 4.3 变量与表达式 4.5 函数与方法 4.6 状态 4.7 函数式编程的特性 4.7.1 ...

  4. 什么是反应式编程(超详细说明),反应式编程和命令式编程的区别。如何使用Spring中的Reactor。Reactor中常用的操作。Mono和Flux。

    文章目录 一.反应式编程初探 什么是反应式编程 为什么需要反应式编程? 反应式编程的规范 二.上手反应式编程(使用Spring中的Reactor) 对比反应式编程和命令式编程代码 添加相应依赖 Mon ...

  5. 功能性,声明式和命令式编程[关闭]

    功能,声明和命令式编程这两个术语是什么意思? #1楼 命令式 - 表达式描述要执行的动作序列(关联) 声明性 - 表达式是有助于程序行为的声明(关联,交换,幂等,单调) 功能 -词汇具有值作为唯一的影 ...

  6. 速查笔记(Linux Shell编程上)

    零.shell中的内部变量: 1.    $?:    表示shell命令的返回值. 2.    $$:    表示当前shell的pid. 3.    $!:    最后一个放入后台作业的PID值. ...

  7. 游戏之旅 我的编程感悟_我的外展之旅的特别时刻

    游戏之旅 我的编程感悟 by Toni Shortsleeve 通过托尼·肖特里夫(Toni Shortsleeve) 我的外展之旅的特别时刻 (Special Moments on my Outre ...

  8. 声明式和命令式编程_命令式与声明式编程

    声明式和命令式编程 At this point you've undoubtedly heard about imperative programming vs declarative program ...

  9. react组件卸载调用的方法_React调用子组件方法与命令式编程误区

    本文将阐述以下内容: 调用DOM元素方法 调用React子组件方法的两种直接方案 自省组件结构设计是否合理 -- 探讨声明式编程与命令式编程在React开发中的问题 调用React子组件方法的最佳方案 ...

最新文章

  1. 【AGC+FPGA】基于FPGA的数字AGC自适应增益设计,应用在BPSK调制解调系统中
  2. 用pulse generator产生脉冲信号
  3. Vue+Openlayer使用Draw实现交互式绘制线段
  4. 五分钟了解一致性hash算法!
  5. Android之解决ubuntu没有无线网卡和手机wifi实现adb wifi调试
  6. Oracle10g数据库的树立
  7. Windows server 2012 hyper-v 的实时迁移
  8. Win2008r2 由ESXi 转换到 HyperV的处理过程
  9. 使用Project进行项目管理
  10. 自走棋突然显示服务器无法定位,刀塔自走棋服务器无法定位游戏会话_刀塔自走棋服务器无法定位游戏会话怎么回事_玩游戏网...
  11. linux上的客户端连接window上的服务器
  12. Final Cut Pro中文教程 (1) 基础认识Final Cut Pro
  13. 应用签名不一致,该应用可能被篡改的问题
  14. 机器学习数据预处理----分类型文字数据的处理
  15. YTU 3795 GCD 和 LCM
  16. linux服务器磁盘空间不足导致tar失败
  17. 苹果换卡后显示无服务器,苹果手机插上sim卡无服务怎么办
  18. MySQL表字段类型哪些
  19. 江苏省计算机应用基础统考,年江苏省普通高“专转本”统一考试计算机应用基础.doc...
  20. HTML 页面元素介绍

热门文章

  1. VS2013自带的Browser Link功能引发浏览localhost网站时不停的轮询
  2. 灰度图像--图像分割 Scharr算子
  3. va_start() va_end()函数应用
  4. Winson.SqlPager 2.5 发布!
  5. 【C++】【三】单向链表
  6. GCC生成的汇编代码
  7. asp.net 2.0中一次性更新所有GRIDVIEW的记录
  8. ASP.net 中的页面继承实现和通用页面的工厂模式的实现
  9. Gin源码解析和例子——路由
  10. 一种注册表沙箱的思路、实现——研究Reactos中注册表函数的实现1