首页
社区
课程
招聘
[推荐]F#语言
2010-10-23 15:47 12101

[推荐]F#语言

2010-10-23 15:47
12101
F#语言基于函数编程语言概念。函数编程语言将计算看做是数学函数的赋值,对于需要使用大量数学符号的某些领域专业人员来说,F#语言的数学特性颇具吸引力。Somasegar表示,F#的目标是成为.Net上的“上等公民”。
        微软研究院也曾对F#语言做出过解释,F#语言集安全、性能、脚本与Modern Runtime系统(Java虚拟机和微软通用Runtime)等多种优势于一体,并支持Python等交互式脚本语言、强类型推理、ML的安全性。另外,F#语言可以访问access.net库以及其他数据库工具软件。

c#面向对象的

使用 .NET Framework 中的函数式编程技术
F# :http://msdn.microsoft.com/zh-cn/fsharp/default(en-us).aspx
本文讨论:

安装 F#
F# 语言基础
.NET 互操作性
异步 F#

作为 Microsoft? .NET Framework 家族的新成员,F# 提供类型安全、性能以及类似脚本语言的工作能力,所有这些都是 .NET 环境的一部分。此函数式语言由 Microsoft 研究院的 Don Syme 发明,作为 CLR 的 OCaml 语法兼容变体,但 F# 已经迅速地从科研转为投入实际应用。
随着函数式编程的概念通过 .NET 泛型和 LINQ 等技术越来越多地渗入主流语言(例如 C# 和 Visual Basic?),F# 在 .NET 社区里的知名程度也不断提高——因此,2007 年 11 月 Microsoft 宣布将 F# 确定为受支持的 .NET 编程语言。
多年来,大家一直认为函数式语言领域(ML、Haskell 等)更适合用于学术研究,而不适用于专业开发。但这并不代表这些语言没有过人之处。事实上,.NET 的一些重要的功能增强(例如泛型、LINQ、PLINQ 和 Futures)都是将一些函数式编程概念全新应用到语言所致。以往对这些语言的关注程度不高主要是因为它们的目标平台与专为 Windows? 编写程序的开发人员关系不大、不能与底层平台很好集成,或者不支持关系数据库访问、XML 解析和进程外通信机制等主要功能。
但是,CLR 及其“多种语言,单一平台”的方法将使此类语言在 Windows 开发中的应用越来越广泛。并且顺理成章地引起在一线工作的程序员们的注意。F# 即是这样一门语言。在本文中,我将为您介绍一些 F# 的基本概念和优点。然后,为了帮助您初步了解 F#,我将详细介绍它的安装过程并编写几个简单的小程序。

为什么要使用 F#?
对于小部分 .NET 程序员来说,学习一门 .NET Framework 函数化语言无疑将使自己在编写功能强大软件方面前进一大步。而对其他程序员来说,学习 F# 的理由就因人而异了。F# 能为开发人员提供哪些益处?
随着多核 CPU 的普及,安全并发程序已成为过去三年来的关注焦点。(1)函数式语言倡导一种固定不变的数据结构,可在线程和机器之间传递,而无需担心线程安全或原子访问,开发人员可以利用这一特点支持并发操作。(2)函数式语言还可更轻松地编写更支持并发特性的库,如稍后将在本文中介绍的 F# 异步工作流。
尽管对于专攻面向对象开发的程序员而言,可能对这种语言感觉不是这么强烈,但在很多情况下,函数式程序确实可以简化某些应用程序的编写和维护。例如,编写一个将 XML 文档转换成其他格式数据的程序。虽然完全可以通过编写一个 C# 程序,让它解析整个 XML 文档并应用各种 if 语句确定在文档中的不同位置采取何种措施,但实际上更好的方法是编写可扩展样式表语言转换 (XSLT) 程序。当然,XSLT 肯定包含大量的内置函数机制,如同 SQL 一样。
(3)F# 强烈建议不要使用空值 (null),而是提倡使用固定不变的数据结构。这些特性可以减少需要编写的特例代码量,从而有助于降低编程出错的频率。
(4)使用 F# 编写的程序还更加简洁。您可以切实地从两方面减少键入的内容:击键次数更少并且必须要向编译器通告变量类型、参数或返回类型的位置点也更少。这意味着需要维护的代码将大大减少。
(5)F# 具有与 C# 相似的性能特点。但是,与简洁程度相似的语言(特别是那些动态和脚本语言)相比,它的性能特点要好得多。并且,F# 也包含通过编写程序段并交互式执行查看数据的工具,这一点与许多动态语言类似。

安装 F#
F# 可从 research.microsoft.com/fsharp/fsharp.aspx 免费下载,它不仅会安装所有命令行工具,而且还会安装 Visual Studio? 扩展软件包,该软件包提供彩色语法突出显示、项目和文件模板(包括作为入门指南的详细 F# 示例代码)以及 IntelliSense? 支持。同时它还提供可在 Visual Studio 内部运行的 F# 交互式 shell,它使开发人员能够从源文件窗口中提取表达式、将表达式粘贴到交互式 shell 窗口,并立即得到代码段的结果——在类似增强的 Immediate 窗口中显示结果。
在我撰写本专栏时,F# 在 Visual Studio 内作为外部工具运行,所以它缺少某些开发人员能够从 C# 或 Visual Basic 中获得的无缝集成的能力。此外,F# 还缺少 ASP.NET 页面设计器支持。(这并不是说 F# 不能在 ASP.NET 中使用,完全不是。这仅表示 Visual Studio 并没有为 F# 提供类似 C# 和 Visual Basic 的那种现成拖放式开发体验。)
尽管如此,当前版本的 F# 还是可以在能够使用其他 .NET 兼容语言的任何位置使用。在接下来的几页中,您将看到一些示例。

您好,F#
介绍任何语言的特有方式就是通过那几乎成为标准的“Hello, World”程序。F# 也不例外:
复制代码
printf "Hello, world!"
虽然不能引起您太大的兴趣,但这个很小的例子显示出 F# 属于不需要显式入口点(C#、Visual Basic 和 C++/CLI 都需要显式入口点)的语言;该语言假设程序的第一行即为入口点并将从此处开始执行。
要运行此程序,刚入门的 F# 开发人员有两个选择:编译或解释。在 F# 解释程序中运行此程序 (fsi.exe) 很简单。只需从命令行启动 fsi.exe 并在出现的提示中输入上面一行内容即可,如图 1 所示。
图 1在 F# 解释程序中运行 'Hello, World' (单击该图像获得较大视图)
请注意,在 shell 中,每条语句必须以两个分号结尾。这是交互模式的特殊要求,编译过的 F# 程序并不需要使用这种方式。
要将此示例作为标准的 .NET 可执行程序运行,请正常启动 Visual Studio 并创建一个新的 F# 项目(可以在“其他项目类型”中找到这个项目)。最初,F# 项目包含一个 F# 源文件,称为 file1.fs。打开此文件将发现大量示例 F# 代码集合。浏览一下这些内容即可大概了解 F# 的语法结构。然后将整个文件替换成前面所显示的 "Hello, world!" 代码。运行此应用程序,毫无疑问,在控制台应用程序窗口中将出现“Hello, world!”。
如果更喜欢命令行方式,可以使用 F# 安装目录中 \bin 子目录里的 fsc.exe 工具编译这段代码。请注意:fsc.exe 与大多数命令行编译器的工作方式相似,它从命令行获取源代码并生成可执行程序作为结果。大多数命令行开关都有相关文档,如果曾经使用过 csc.exe 或 cl.exe 编译器,那您可能已经熟悉其中许多开关。不过请注意,F# 目前对 MSBuild 的支持尚不完美;在当前安装版本(撰写本文时为 1.9.3.7)中不能直接支持由 MSBuild 驱动的编译。
如果希望“Hello, world!”中更具图形化特点,F# 可以通过 CLR 平台(包括 Windows Forms 库)轻松地提供完整的逼真度和互操作性。尝试以下代码:
复制代码
System.Windows.Forms.MessageBox.Show "Hello World"
利用 .NET Framework 类库和 F# 库的能力使得 F# 语言不仅对那些早已使用如 OCaml 或 Haskell 之类函数式语言进行数学和科学计算的社区极具吸引力,而且受到全世界现有 .NET 开发人员的青睐。

Let 表达式
让我们看看比传统“Hello, world!”更复杂一些的 F# 代码示例。请看以下代码:
复制代码
let results = [ for i in 0 .. 100 -> (i, i*i) ]
printfn "results = %A" results
在该 F# 语法中,let 表达式是令人产生好奇的元素。它是整个语言中最重要的表达式。更正式地说,let 可以为标识符赋值。用 Visual Basic 和 C# 开发人员的行话来说,“它可以定义一个变量”。但这并不确切。在 F# 中,标识符包含两个要素。首先,标识符一旦定义就不能再更改。(这即是 F# 帮助程序员创建并发安全程序的方法,因为它不提倡可变的状态。)第二,标识符不仅可以是基元类型或对象类型(如 C# 和 Visual Basic 中所使用的类型),而且还可以是函数类型,这一点与 LINQ 相似。
同时还请注意,标识符从不显式定义为类型。例如,从不定义结果标识符;它将从后面表达式的右侧进行推断。这称为类型推断,并且它代表编译器分析代码、确定返回值和自动插入返回值的能力(这与新的 C# 推断类型表达式通过变量关键字进行推断的方法类似)。
Let 表达式不仅可以处理数据,还可以使用它来定义函数,F# 将函数看作第一级概念。下面的示例定义了一个加法函数,它使用两个参数 a 和 b:
复制代码
let add a b =
    a + b
完全按照您预期的方式工作:将 a 与 b 相加,并将结果显式返回给调用者。这意味着从技术上讲,F# 中的每个函数都将返回一个值,即使返回的不一定是值,也会返回一个特殊的名称 unit。这将在 F# 代码中产生一些有趣的暗示,特别是与 .NET Framework 类库相交的部分。但目前,C# 和 Visual Basic 开发人员可以把 unit 大致看作是与 void 相同的类型。
有时函数应该忽略传递给它的参数。要在 F# 中达此目的,仅需使用下划线作为该参数的占位符即可:
复制代码
let return10 _ =
    add 5 5

// 12 is effectively ignored, and ten is set to the resulting
// value of add 5 5
let ten = return10 12

printf "ten = %d\n" ten
与许多函数式语言类似,F# 允许根据其调用进行 currying(可以仅部分定义函数的应用),以便提供其余的参数:
复制代码
let add5 a =
    add a 5
在某种程度上,这与创建一个接受不同的参数集并调用其他方法的重载方法相似:
复制代码
public class Adders {
    public static int add(int a, int b) { return a + b; }
    public static int add5(int a) { return add(a, 5); }
}
但两者还是有一点细微的差别。请注意,在 F# 版本中,没有显式定义类型。这表示编译器将采用自己的类型推断方法确定 add5 的参数是否是与加上整数 5 兼容的类型,并确定是按照这种方式编译,还是将其标记为错误。事实上,F# 语言主要使用隐式类型参数化(即使用泛型)。
在 Visual Studio 中,将指针停放在前面所显示的 ten 的定义上时,将表明其类型声明为:

val ten : ('a -> int)
在 F# 中,这表示 ten 是一个值,一个获取任意类型的参数并产生整数结果的函数。这种记号语法大致等同于 C# 中的 <T> 语法,所以对 C# 函数最贴切的说法是:ten 是类型参数化方法的委托实例,您想要忽略其类型(但在 C# 规则下不可忽略):

delegate int Transformer<T>(T ignored);

public class App
{
  public static int return10(object ignored) { return 5 + 5; }

  static void Main()
  {
    Transformer<object> ten = return10;
    System.Console.WriteLine("ten = {0}", return10(0));
  }
}


关键字 For
现在让我们看一下第一个示例中的 for 关键字:
#light

let results = [ for i in 0 .. 100 -> (i, i*i) ]
printfn "results = %A" results
先看代码的顶部,注意 #light 语法。这是为非 OCaml 程序员开始使用 F# 而做的让步,放宽某些 OCaml 语言的语法要求,并使用大量空白定义代码块。虽然不一定必要,但对于那些原本使用 C# 或 Visual Basic 的普通开发人员来说,它确实使语法更易于解析,因此它常出现在 F# 示例和公开的代码段中,并已成为事实上的 F# 编程标准。(未来版本的 F# 可能会将 #light 确定为默认语法,代替其他类似方法。)
那个看似简单的 for 循环实际上并不简单。正式地说,这是生成列表,即将生成列表型结果的代码块的另一种说法。
列表是函数式语言中经常出现的一种原语结构,在这方面它与数组有许多相似之处。但是,列表不允许基于位置的访问(如 C# 中传统的 a 语法)。列表可在函数式编程的不同位置出现,大多数情况中,可以将其看作是 F# 中与 .NET Framework 中的 List<T> 类似,但提供一些增强功能的同等项。
列表通常使用一些特殊类型,本例中标识符结果是聚合列表,特别地是 F# 将此聚合类型标识为类型 (int * int)。如果将此聚合列表看作是 SQL 中 SELECT 语句返回的一对列,它的含义就清楚多了。因此,示例本质上是创建一个包含 100 个项目的整数对列表。
通常,在函数式语言中,函数定义可以在出现代码的任何位置使用。因此,如果希望扩展前面的示例,可以编写以下代码:

let compute2 x = (x, x*x)
let compute3 x = (x, x*x, x*x*x)
let results2 = [ for i in 0 .. 100 -> compute2 i ]
let results3 = [ for i in 0 .. 100 -> compute3 i ]
遍历列表(或数组或其他一些重复结构)是函数式语言中很常见的任务,它已成为基本方法调用:List.iter。它仅简单地对列表中的每个元素调用一个函数。其他类似的库函数还可提供一些非常有用的功能。例如,List.map 将函数作为参数,并将该函数应用于列表中的每个元素,并返回该过程产生的新列表。

[培训]内核驱动高级班,冲击BAT一流互联网大厂工 作,每周日13:00-18:00直播授课

收藏
点赞0
打赏
分享
最新回复 (3)
雪    币: 189
活跃值: (11)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
adomore 2010-10-23 15:49
2
0
管道
让我们讨论 F# 中另一个结构——管道操作符,它通过类似命令 shell(如 Windows PowerShell?)管道的通道获取函数的结果,并将结果用作后一个函数的输入。我们来看图 2 中显示的 F# 代码段。该代码段使用 System.Net 命名空间连接 HTTP 服务器,获取相应的 HTML 并分析结果。
Figure 2 检索和分析 HTML
复制代码
/// Get the contents of the URL via a web request
let http(url: string) =
  let req = System.Net.WebRequest.Create(url)
  let resp = req.GetResponse()
  let stream = resp.GetResponseStream()
  let reader = new System.IO.StreamReader(stream)
  let html = reader.ReadToEnd()
  resp.Close()
  html
let getWords s = String.split [ ' '; '\n'; '\t'; '<'; '>'; '=' ] s
let getStats site =
  let url = "http://" + site
  let html = http url
  let words = html |> getWords
  let hrefs = html |> getWords |> List.filter (fun s -> s = "href")
  (site,html.Length, words.Length, hrefs.Length)

请注意 getStats 定义中的 words 标识符。它获取从 URL 返回的 html 值,并对其应用 getWords 函数。我还可以编写定义读取:
复制代码
let words = getWords html
两者等同。但是 hrefs 标识符显示了管道操作符的威力,通过管道操作符可以将任意多个应用程序连接起来。此处我获取 words 的结果列表,并将其通过管道传递给 List.filter 函数,该函数使用匿名函数查找单词 href,并在表达式为 true 时将其返回。并且,最重要的是,getStats 调用的结果将是另一个聚合 (string * int *int * int)。要使用 C# 编写,需要的远远不止 15 行代码。
图 2 中的示例还显示出更多 F# 与 .NET Framework 的兼容性,以下代码也表现出这一特性:
复制代码
open System.Collections.Generic
let capitals = Dictionary<string, string>()
capitals.["Great Britain"] <- "London"
capitals.["France"] <- "Paris"
capitals.ContainsKey("France")
确实,这个示例除了练习 Dictionary<K,V> 类型外没有什么其他内容,但它显示出在 F# 中如何指定泛型(使用与 C# 相同的尖括号)、如何在 F# 中使用索引(同样与 C# 一样使用方括号),以及如何执行 .NET 方法(使用与 C# 中同样的点和圆括号)。事实上,这里仅有的新内容是使用左箭头操作符为可变值赋值。这一点是必需的,因为 F# 与大多数函数式语言一样,保留等号用于比较,以便保持数学符号含义:如果 x = y,则 x 与 y 的值相等,而不是将 y 的值赋给 x。(真正的数学家们早已对普遍存在或设想过的 x = x + 1 提出异议,甚至偷笑不已。)

F# 也能够处理对象
当然,并不是所有开始使用 .NET 的开发人员都愿意立即接受函数式的概念。事实上,大多数从 C# 或 Visual Basic 转向 F# 的开发人员都需要知道他们在使用这一新语言时可以保留原有的习惯。在某种程度上,这是完全可行的。
例如,请看图 3 顶部所示的二维向量的类定义。其中就有一些有趣的概念。首先,请注意其中没有显式构造函数体;第一行中的参数表明用于构造 Vector2D 实例的参数本质上就是构造函数。因此长度标识符,以及 dx 和 dy 标识符将成为 Vector2D 类型的私有元素,而 member 关键字则表明可以通过标准 .NET 属性访问获取的 Vector2D 外部可用成员。本质上,这段 F# 代码声明了您可在图 3 底部看到的内容(由 Reflector 报告)。
Figure 3 F# 和 C# 中的矢量变体
复制代码
            VECTOR2D IN F#
type Vector2D(dx:float,dy:float) =
    let length = sqrt(dx*dx + dy*dy)
    member obj.Length = length
    member obj.DX = dx
    member obj.DY = dy
    member obj.Move(dx2,dy2) = Vector2D(dx+dx2,dy+dy2)

VECTOR2D IN C# (REFLECTOR>
[Serializable, CompilationMapping(SourceLevelConstruct.ObjectType)]
public class Vector2D
{
  // Fields
  internal double _dx@48;
  internal double _dy@48;
  internal double _length@49;

  // Methods
  public Vector2D(double dx, double dy)
  {
    Hello.Vector2D @this = this;
    @this._dx@48 = dx;
    @this._dy@48 = dy;
    double d = (@this._dx@48 * @this._dx@48) +
               (@this._dy@48 * @this._dy@48);
    @this._length@49 = Math.Sqrt(d);
  }

  public Hello.Vector2D Move(double dx2, double dy2)
  {
    return new Hello.Vector2D(this._dx@48 + dx2, this._dy@48 + dy2);
  }

  // Properties
  public double DX
  {
    get
    {
      return this._dx@48;
    }
  }

  public double DY
  {
    get
    {
      return this._dy@48;
    }
  }

  public double Length
  {
    get
    {
      return this._length@49;
    }
  }
}
请记住,F# 与大多数函数式语言相似,提倡使用不变的值和状态。当查看图 3 中的代码时,这一点尤为明显,因为所有属性都为只读属性,并且 Move 成员不会修改现有的 Vector2D,而是从当前 Vector2D 创建新的副本,并在返回副本之前对其应用修改的值。
还请注意,F# 版本不仅具备整体线程安全性,而且完全可以通过传统的 C# 或 Visual Basic 代码进行访问。这为 F# 入门提供了一种简便方法:使用它定义想要或者需要线程安全和固定不变的业务对象或其他类型。虽然完全可以在 F# 中创建提供常用可变操作组(设置属性及类似操作)的类型,但需要更多的工作,而且需要使用 mutable 关键字才可完成。在当今并发问题成为日常工作主旋律的世界中,这正如许多人所要求的一样——默认固定不变,必需或想要时可变。
在 F# 中创建类型很有趣,但还是可以用 F# 去做那些传统 C# 或 Visual Basic 代码可以做到的工作,如创建简单的 Windows 窗体应用程序并从用户处收集输入,如图 4 所示。
Figure 4 使用 F# 编写 Windows 窗体
复制代码
#light

open System
open System.IO
open System.Windows.Forms
open Printf

let form = new Form(Text="My First F# Form", Visible=true)

let menu = form.Menu <- new MainMenu()
let mnuFile = form.Menu.MenuItems.Add("&File")
let filter = "txt files (*.txt)|*.txt|All files (*.*)|*.*"

let mnuiOpen =
  new MenuItem("&Open...",
    new EventHandler(fun _ _ ->
      let dialog =
        new OpenFileDialog(InitialDirectory="c:\\",
          Filter=filter;
          FilterIndex=2,
          RestoreDirectory=true)
        if dialog.ShowDialog() = DialogResult.OK then
          match dialog.OpenFile() with
          | null -> printf "Could not read the file...\n"
          | s ->
            let r = new StreamReader(s)
            printf "First line is: %s!\n" (r.ReadLine());
            s.Close();
    ),
    Shortcut.CtrlO)

mnuFile.MenuItems.Add(mnuiOpen)

[<STAThread>]
do Application.Run(form)

任何熟悉 Windows 窗体的开发人员都能够立即明白这些代码的含义:创建一个简单的窗体、填充一些属性、填入一个事件处理程序,并告诉应用程序开始运行,直到用户单击右上角的“关闭”按钮。由于标准元素与 .NET 应用程序相同,所以只需重点关注 F# 语法即可。
Open 语句的操作与 C# 中 using 语句的作用大致相同,本质上都是打开 .NET 命名空间以便在没有正式限制符的情况下使用。Printf 命名空间是 F# 原有的、技术上与 OCaml 模块具有相同名称的端口。F# 不仅具备完整的 .NET Framework 类库,而且还有最简洁的 OCaml 库端口,这使得熟悉该语言的程序员能够象使用 .NET Framework 一样对其运用自如。(致好奇心强的程序员:Printf 位于 FSharp.Core.dll 程序集中。)您完全可以根据个人偏好随时使用 System.Console.WriteLine。
窗体标识符的创建利用了 F# 命名参数,它等同于实例化对象,然后调用一系列属性集来为这些属性填充值。我在下面的几行中使用相同的方法创建对话框标识符。
mnuiOpen 标识符的定义包含令人感兴趣的结构,该结构对于熟悉 .NET Framework 2.0 匿名委托或 .NET Framework 3.5 中 lambda 表达式的开发人员来说并不陌生。构造与 Open MenuItem 关联的 EventHandler 时,您可以看到使用以下语法定义的匿名函数:
复制代码
fun _ _ -> ...
类似于匿名委托,这段代码创建了一个将会在选中菜单项时调用的函数,但语法略有技巧性。
MenuItem 定义中对 EventHandler 的定义是忽略传递给它的两个参数的匿名函数,这两个参数巧妙地对应标准 EventHandler 委托类型中的发送方和事件参数。该函数规定显示新的 OpenFileDialog 并在单击“确定”时检查结果...如下所示:
复制代码
if dialog.ShowDialog() = DialogResult.OK then
    match dialog.OpenFile() with
    | null -> printf "Could not read the file...\n"
    | s ->
        let r = new StreamReader(s) in
        printf "First line is: %s!\n" (r.ReadLine());
        s.Close();
将使用模式匹配检查结果,该方法是函数化语言世界中一项强大的功能。模式匹配表面上与 C# 中的 switch/case 在某些地方存在相似之处,但实际上它名副其实地完成模式匹配工作:它将值与各种不同的模式进行比较(这些模式不需要都是常量值),并执行匹配的代码块。因此,以此处所示的匹配块为例,OpenFile 的结果可以匹配两种可能的值:null 表示无法打开任何文件,或者分配任何非 null 值的 s,该值将随后用作 StreamReader 的构造函数来打开并读取给定文本文件的第一行。
模式匹配是大多数函数式语言的重要部分,对它做些研究是完全值得的。它的一个最常见的用途是与可辨识联合 (discriminated union) 类型(C# 或 Visual Basic 中枚举类型的不确切说法)配合使用:
复制代码
// Declaration of the 'Expr' type
type Expr =
  | Binary   of string * Expr * Expr
  | Variable of string
  | Constant of int

// Create a value 'v' representing 'x + 10'
let v = Binary("+", Variable "x", Constant 10)
函数式语言中常用它来创建域特定语言的核心表示,开发人员可以使用它来编写更为复杂和强大的结构。例如,不难想象扩展此语法以创建完全计算式语言,并可简单地通过为 Expr 类型添加新元素而进一步扩展该语言。这里需要注意的是:使用 * 字符的语法并不表示使用乘法,它是函数式语言中用于指示某类型中包含多个部分的标准方式。
事实上,函数式语言已经非常普遍地应用于编写面向语言的编程工具(如解释器和编译器),并且 Expr 类型最终将成为语言表达式类型的完整集合。F# 通过其内置的两个工具:fslex 和 fsyacc(专为获得传统语言输入—lex 和 yacc 文件—并将其编译成 F# 代码以便简化操作而设计)使这一切变得更为简单。如果对此感兴趣,可以下载 F# 安装程序深入研究;特别是标准 F# 发行包中的 Parsing 示例将提供非常好的入门基础结构。
可辨识联合只是模式匹配的优势之一,另一项优势是表达式的执行,如图 5 所示。位于 eval 定义中的 rec 是必需的,它告诉 F# 编译器在定义主体内迭代调用 eval。如果没有它,F# 将期望出现一个名为 eval 的本地嵌套函数。实际使用时,我使用函数 getVarValue 为变量返回一些预定义的值,getVarValue 将检查 Dictionary,查找变量创建时确定的返回值。
Figure 5 表达式执行
复制代码
let getVarValue v =
    match v with
    | "x" -> 25
    | "y" -> 12
    | _ -> 0

let rec eval x =
    match x with
    | Binary(op, l, r) ->
        let (lv, rv) = (eval l, eval r) in
        if   (op = "+") then lv + rv
        elif (op = "-") then lv - rv
        else failwith "E_UNSUPPORTED"
    | Variable(var) ->
        getVarValue var
    | Constant(n) ->
        n

do printf "Results = %d\n" (eval v)

当调用 eval 时,它将得到值 v 并发现该值是一个 Binary 值。这与第一个子表达式匹配,该表达式随后把值 (lv, rv) 绑定到刚检查的 Binary 值左右两侧的计算结果。未命名的值 (lv, rv) 是一个聚合(本质上是代表多个部分的单个值),这一点与关系集或 C 结构相似。
当首次调用 eval l 时,来自 Binary 实例的 l 恰好是 Variable 类型,因此对 eval 的递归调用匹配该模式匹配块的分支。随后将调用 getVarValue,它会返回硬编码 25,该值最终将绑定到值 lv。对于包含值 10 的常量 r 来说顺序相同,因此它将绑定到 rv。然后执行代码块的剩余部分(if/else-if/else 块),熟悉 C#、Visual Basic 或 C++ 的开发人员可以很容易地读懂该代码块的含义。
这里需要再次强调的是:每个表达式都将返回一个值,甚至在模式匹配块内部也一样。在本例中,返回值是一个整型值,该值可能是运算得到的值、从变量中检索到的值或者是常量本身。这一点似乎更容易让习惯于面向对象或过程化编程的开发人员产生微词,因为在 C#、Visual Basic 或 C++ 中,返回值是可选的,并且甚至在指定返回值的情况下仍可以忽略返回值。在类似 F# 的函数式语言中,要忽略返回值需要显式编码表达方式。如果出现这种情况,程序员可以将结果传给名为 ignore 的函数,由它完成适当的操作。

异步 F#
目前为止,我对 F# 语法的介绍采用以下两种方式中的一种:或者使用相对简单的函数式结构,或者使其看起来比较初级且简洁,象是传统面向对象、.NET 兼容语言(C#、Visual Basic 或 C++/CLI)的变体。这种介绍很难推动在企业内采用 F#。
但是请看一下图 6。它可与前面两种形式截然不同。除了多处出现 ! 字符并使用 async 修饰符外,这是一段看起来相对比较直观的代码:加载源图像映像、提取其数据、将数据传递到独立的函数进行加工(旋转、拉伸或其他操作),并将数据写回输出文件。
Figure 6 处理图像
复制代码
let TransformImage pixels i =
  // Some kind of graphic manipulation of images

let ProcessImage(i) =
  async { use  inStream  = File.OpenRead(sprintf "source%d.jpg" i)
          let! pixels    = inStream.ReadAsync(1024*1024)
          let  pixels'   = TransformImage(pixels,i)
          use  outStream = File.OpenWrite(sprintf "result%d.jpg" i)
          do!  outStream.WriteAsync(pixels')
          do   Console.WriteLine "done!"  }

let ProcessImages() =
  Async.Run (Async.Parallel
               [ for i in 1 .. numImages -> ProcessImage(i) ])

较不明显的是使用 async 修饰符使这段代码进入 F# 所称的异步工作流(与 Windows Workflow Foundation 无关)中,这意味着这些加载/处理/保存步骤的每一步都在 .NET 线程池的平行线程中执行。
为了使其更简单,看一下图 7 中的代码。这种特殊的顺序以相对简单且易于理解的方式显示出异步工作流。不用深究细节,我们就可以看出 evals 是一组待执行的函数,通过 Async.Parallel 调用使其中每个函数都在线程池中排队等待执行。当执行时,可以看出实际上 evals 中的函数与 awr 中的函数在不同的线程中(尽管由于 .NET 系统线程池的特性,部分或全部 evals 函数有可能在相同的线程中执行)。
Figure 7 异步执行函数
复制代码
#light
open System.Threading

let printWithThread str =
    printfn "[ThreadId = %d] %s" Thread.CurrentThread.ManagedThreadId str
   
let evals =
    let z = 4.0
    [ async { do printWithThread "Computing z*z\n"
              return z * z };
      async { do printWithThread "Computing sin(z)\n"
              return (sin z) };
      async { do printWithThread "Computing log(z)\n"
              return (log z) } ]

let awr =
    async { let! vs = Async.Parallel evals
            do printWithThread "Computing v1+v2+v3\n"
            return (Array.fold_left (fun a b -> a + b) 0.0 vs) }

let R = Async.Run awr
printf "Result = %f\n" R

这些函数在 .NET 线程池以外执行再次表明 F# 语言对运行时互操作性的支持非常出色。甚至函数式语言中以前专门为特殊实现(如线程)保留的领域也要依赖 .NET Framework 类库,这表明 C# 程序员能够使用 F# 库或模块,就像 F# 开发人员能够使用 C# 库一样。事实上,将来 F# 的功能(如异步任务)能够充分利用新的 .NET Framework 库(例如并行扩展库中的任务处理库)。

与 F# 合作
如果对 F# 还不是十分清楚,那还有许多有关 F# 语言的介绍,这些内容远远超出一篇文章所能容纳的范围;事实上,在新语法和全新的思考方式(函数式与过程式)之间,坦白说对于已经习惯 C# 或 Visual Basic 等面向对象的普通开发人员来说,要掌握 F# 还需要一些时间。幸运的是,F# 与 .NET 系统中的其他语言可完全互操作,这表示您可以利用许多现有的知识和工具帮助您将 F# 变为自己的编程工具。
F# 开发人员可以访问所有的基类库,并且由于 F# 支持一些过程式和面向对象式开发,完全可以考虑不经过编译,使用 F# 交互模型学习 F# 语法和 Windows Presentation Foundation、Windows Communication Foundation 或 Windows Workflow Foundation 的细节。
如前所述,开发人员可以在 F# 中编写供应用程序代码其他部分使用的业务对象。因为 F# 类型构造生成的类大部分与使用 C# 或 Visual Basic 构造的类相似,所以 NHibernate 这类的持久型库仍可顺利保留 F# 类型,从而使 F# 能够无缝注入其他正在使用的业务应用程序中。
只需学习一些 F# 的知识就可帮助您更好地理解 C# 和 Visual Basic 未来版本中的许多新特性,因为其中许多想法和概念——包括泛型、迭代器(C# 中的 yield 关键字)和 LINQ——都来源于函数式和 F# 团队所做的研究。无论您怎么看待函数式编程,它都已是既成的事实,并且还将继续发挥功能。
雪    币: 189
活跃值: (11)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
adomore 2010-10-23 15:51
3
0
(一)Notepad
F#程序本质上就是文本文件,所以我们可以使用文本编辑器来编写,比如记事本。它的文件扩展名为.fs,编写完毕后使用fsc.exe来编译。比如,编写一个最简单的文件helloworld.fs:
#light

print_endline "Hello World"
read_line()

使用命令fsc.exe helloworld.fs编译该文件,生成helloworld.exe文件,它将在控制台输出一段文本。注意要将fsc.exe的路径添加到环境变量中。
如果采用文本编辑器的方式,我们当然不会真的使用Notepad,大可以采用Editplus或Notepad++这样的工具,它们不但提供了更强大的编辑功能,还可以添加用户自定义工具,这样就不用每次都打开命令行编译了。
(二)FSI
F#交互控制台(F# Interactive Console, FSI)采用的是“REPL loop”模式,即Read-Evaluate-Print-Loop。也就是输入一段代码,编译并执行,然后输出结果。通过它您可以快速地开发和测试程序。可在开始菜单中找到它。
(三)VFSI(在VS中集成FSI)
要在VS中启用FSI,打开Add-in Manager窗口。

选中“F# Inactive for Visual Studio”。然后,选中要执行的程序代码:

接着按下Alt+Enter(实际上,如果FSI还么有打开,需要按两次Alt+Enter)。这时会看到出现了一个工具窗口:

我们刚才做的事情是将代码片段直接发送给FSI会话,FSI将结果输出。
这是最简单的方式了,我们可以利用VS提供的智能感知,可以查看各个标识符的类型等信息,非常方便。
另外我们可以将fsc.exe添加到外部工具中,为该命令分配快捷键。比如可以这样添加:

然后分配快捷键:

有时FSI会莫名地不能编译通过,通过这样的方式可以方便的将单个文件编译为可执行文件,编译也不会有问题了。
(四)F# Script
除了.fs,我们还可以将代码文件保存为.fsx,在资源管理器中右击该文件,会发现“Run with F# Interactive”菜单项,选择它就可以执行代码了,这种方式适合于小型文件,可以随时修改,无须编译。
(五)SharpDevelop
最新版的SharpDevelop提供了对F#项目的支持,但是不支持智能感知。我觉得最有用的是它的文件顺序调整功能,如果你的项目包含较多的文件,可以试试它。

F# 20分钟快速上手(一)
第一篇,从零开始编写我们的第一个F#程序。
什么是F#,我为何要学它?
F#是一种.NET平台上的函数式编程语言。就像C#和VB.NET,F#可以利用.NET的核心类库,如WPF,WCF,VSTO等等,通过F#您甚至可以使用XNA编写XBox游戏。
仅仅如此并不意味着您应该去学习它。那为何要使用F#呢?作为一种函数式编程语言,F#使得某些领域的编程要比命令式编程(如使用C#)更为容易。并行编程(Parallel Programming)和面向语言编程(Language-Oriented Programming)是其中的两个领域。
如果您曾经编写过.NET应用程序,并感觉自己在努力使用手头的语言表达自己的想法,也许F#就是您在寻找的。
上路
首先得下载(F#的最新版本1.9.4.19)和安装。安装程序会在VS2005和VS2008中安装F#的项目系统(项目模板和项模板)。先来创建一个新的F#项目。

然后添加一个新的F#源文件。默认条件下,新建的源文件包含了很多“教学”代码,全部删除,然后输入下面的代码:
#light

let square x = x * x
let numbers = [1 .. 10]
let squares = List.map square numbers
printfn "N^2 = %A" squares

open System
Console.ReadKey(true)
按下F5运行程序,您会看到:

这些并没有太多让人兴奋的。我们来逐行的分析下代码,看看到底有什么不同之处,在此之前先介绍下VFSI。
Visual Studio中的F#交互(Interactive)
F#交互控制台(F# Interactive Console, FSI)采用的是“REPL loop”模式,即Read-Evaluate-Print-Loop。也就是输入一段代码,编译并执行,然后输出结果。通过它您可以快速地开发和测试程序。要在VS中启用FSI,打开Add-in Manager窗口。

选中“F# Inactive for Visual Studio”。然后,选中程序的前两行代码:

接着按下Alt+Enter(实际上,如果FSI还么有打开,需要按两次Alt+Enter)。这时会看到出现了一个工具窗口:

我们刚才做的事情是将代码片段直接发送给FSI会话,FSI将结果输出。结果是函数“square”的定义,它接受int类型参数,返回类型也是int。
接下来在FSI窗口输入“List.map square [1 .. 2 .. 10];;”。“;;”是告诉FSI停止阅读程序,立即进行求值。
> List.map square [1 .. 2 .. 10];;
val it : int list = [1; 9; 25; 49; 81]
现在我们可以方便地通过FSI来学习F#了,马上来看看我们的程序究竟做了什么吧。不过仍建议您在VS源代码编辑器中输入代码,使用“Select(原文是Highlight,感觉Select更贴切) + Alt + Enter”将代码片段发送至FSI。
语言基础
#light(OCaml兼容)
F#源自OCaml,具有交互编译OCaml的能力,也就是可以不经修改即可编译简单的OCaml程序。这种能力也带来了令人讨厌的语法。#light(发音为hash-light)是一个编译器指令,可以简化F#的语法。
强烈建议您保持使用#light,您会发现,在大多数F#代码片段中要么会声明它,要么是假定已经声明了它。
let square x = x * x(类型推演)

这行代码定义了一个函数:square,它会求得数字x的平方。考虑一下C#中等价的代码:
public static int square(int x)
{
    return x * x;
}
在C#中,您需要制定参数和返回值的类型信息,而F#则帮您搞定了。这种行为称为类型推演(Type Inference)。
从函数的签名,F#可以知道“square”函数接受一个参数“x”,并且函数返回“x * x”(在函数体内的最后一次求值将作为返回值,因此无须return关键字)。因为很多基元类型都支持*操作,比如byte,uint64,double等,F#默认会使用int类型,有符号的32位整数。
现在考虑下面的代码,它为其中的一个参数提供了“类型注解(type annotation)”,告诉编译器期望的类型。因为x标为“string”,“+”操作只定义在两个string间,因此y也必须为string类型,返回值是两个字符串拼接的结果。
> let concat (x : string) y = x + y;;
val concat : string -> string -> string

> concat "Hello, " "World!";;
val it : string = "Hello, World!"
后面我们将讨论类型推演的更多高级主题,现在您只要享受F#编译器的智能带来的方便就好了。

let numbers = [1 .. 10](F# lists)
这行代码声明了一个列表(list),其元素是从1至10。如果您用的是[|1 .. 10|],F#会创建一个.NET的整型数组(array)。而在F#中,列表是一个不可变的链表(linked list),这也是函数式编程的基础。试着将这些代码输入到FSI中(记住添加“;;”):
// Define a list
let vowels = ['e'; 'i'; 'o'; 'u']

// Attach item to front (cons)
let cons = 'a' :: vowels

// Concat two lists
let sometimes = vowels @ ['y']
我将在本系列的第二篇中更深入地介绍列表。
let squares = List.map square numbers
现在我们有了一个整型列表(numbers)和一个函数(square),我们希望创建一个新的列表,它的每一项是对numbers的每一项进行square运算后的结果。
幸运的是,List.map可以做到。考虑下面的例子:
> List.map (fun x -> x % 2 = 0) [1 .. 10];;
val it : bool list
= [false; true; false; true; false; true; false; true; false; true]
代码(fun x -> x % 2 = 0)定义了一个匿名函数,称为lamdba表达式,接受一个参数x,返回值为表达式“x % 2 = 0”的结果,也就是判断x是否为偶数。
注意我们刚才做的——将一个函数作为参数传递给另一个函数。在C#中这个并不容易。但在F#可以很清楚地表达出来,而且代码很简洁。将函数像值一样传递被称为“一等函数(first order functions)”,也是函数式编程的基础。
printfn "N^2 = %A" squares
printf是打印文本到控制台窗口的一种简单而又类型安全的方式。要更好地了解printf,考虑下面的例子,它打印一个整数、浮点数和字符串。
> printfn "%d * %f = %s" 5 0.75 ((5.0 * 0.75).ToString());;
5 * 0.750000 = 3.75
val it : unit = ()
%d,%f,%s分别是int、float、string的占位符。%A则可用于打印任何值。
Console.ReadKey(true) (.NET互操作)
我们程序的最后一行只是简单地调用了System.Console.ReadKey方法,这样可以让程序在关闭其暂停。因为F#建立在.NET的基础上,您可以在F#中调用任何.NET类库——从正则表达式到WinForms。代码“open System”用于打开命名空间,类似于C#中的using。
现在我们已经有了F#的基础知识,可以继续学习更有趣的基础类型和F#概念了,希望您关注第二篇文章!
雪    币: 189
活跃值: (11)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
adomore 2010-10-23 15:51
4
0
1.不可变性(Immutability)
您也许已经注意到,我一直使用“值(value)”来表示一个标识符(identifier),而不是“变量(variable)”。这是由于默认情况下,F#中的类型是不可变的(immutable),也就是说,一经创建即不可修改。看起来这是一个很大的限制,但是不可变性可以避免某种类型的bug。另外,不可变的数据天然地具备线程安全的特性,这意味着您无需在处理并行情况时担心同步锁的发生。我将在系列的第三篇中介绍异步编程。
如果您确实需要修改数据,可使用F#的mutable关键字,它会创建一个变量(而不是值)。我们可以通过左箭头操作符(<-)来修改变量的值。
> let mutable x = "the original value.";;
val mutable x : string
> printfn "x's value is '%s'" x;;
x's value is 'the original value.'
val it : unit = ()

> x <- "the new one.";;
val it : unit = ()
> printfn "x's value is now '%s'" x;;
x's value is now 'the new one.'
val it : unit = ()
2. 引用值(Reference values,Microsoft.FSharp.Core.Ref<_>)
引用值是另一种表示可修改数据的方式。但它不是将变量存储在堆栈(stack),引用值其实是一个指向存储在堆(heap)上的变量的指针(pointer)。在F#中使用可修改的值时会有些限制(比如不可以在内部lambda表达式中使用)。而ref对象则可被安全地传递,因为它们是不可变的record值(只是它有一个可修改的字段)。
使用引用值时,用“:=”赋一个新值,使用“!”进行解引用。
> let refCell = ref 42;;
val refCell : int ref

> refCell := -1;;
val it : unit = ()

> !refCell;;
val it : int = –1
3. 模块(Modules)
在上篇文章中,我只是随意地声明了几个值和函数。您也许会问,“要把它们放在哪里呢?”,因为在C#中所有一切都要属于相应的类。尽管在F#中,我们仍然可以用熟悉的方式声明标准的.NET类,但它也有模块的概念,模块是值、函数和类型的集合(可以对比一下命名空间,后者只能包含类型)。
这也是我们能够访问“List.map”的原因。在F#库(FSharp.Core.dll)中,有一个名为“List”的模块,它包含了函数“map”。
在快速开发的过程中,如果不需要花费时间去设计严格的面向对象类型体系,就可以采用模块来封装代码。要声明自己的模块,要使用module关键字。在下面的例子中,我们将为模块添加一个可修改的变量,该变量也是一个全局变量。
module ProgramSettings =
    let version = "1.0.0.0"
    let debugMode = ref false

module MyProgram =
    do printfn "Version %s" ProgramSettings.version
    open ProgramSettings
    debugMode := true
4. 元组(Tuples)
元组(tuple,发音为‘two-pull’)表示值的有序集合,而这些值可看作一个整体。按传统的方式,如果您要传递一组相关的值,需要创建结构(struct)或类(class),或者还需要“out”参数。使用元组我们可以将相关的值组织起来,同时并不需要引入新的类型。
要定义一个元组,只要将一组值用逗号分隔,并用圆括号把它们括起来即可。
> let tuple = (1, false, "text");;
val tuple : int * bool * string

> let getNumberInfo (x : int) = (x, x.ToString(), x * x);;
val getNumberInfo : int -> int * string * int

> getNumberInfo 42;;
val it : int * string * int = (42, "42", 1764)
函数甚至可以接受元组为参数:
> let printBlogInfo (owner, title, url) = printfn "%s's blog [%s] is online at '%s'" owner title url;;
val printBlogInfo : string * string * string -> unit

> let myBlog = ("Chris", "Completely Unique View", "http://blogs.msdn.com/chrsmith");;
val myBlog : string * string * string

> printBlogInfo myBlog;;
Chris's blog [Completely Unique View] is online at 'http://blogs.msdn.com/chrsmith'
val it : unit = ()
5. 函数柯里化(Function Currying)
F#提供的一个新奇的特性是可以只接受参数的一个子集,而接受部分参数的结果则是一个新的函数。这就是所谓的“函数柯里化”。比如,假设有一个函数接受3个整数,返回它们的和。我们可以只传入第一个参数,假设值为10,这样我们就可以说将原来的函数柯里化了,而它会返回一个新的函数——新函数接受两个整数,返回它们与10的和。
> let addThree x y z = x + y + z;;
val addThree : int -> int -> int -> int

> let addTwo x y = addThree 10 x y;;
val addTwo : int -> int -> int

> addTwo 1 1;;
val it : int = 12
6. Union类型(Union Types,Discriminated Unions)
考虑下面的枚举值:
enum CardSuit { Spade = 1, Club = 2, Heart = 3, Diamond = 4};
理论上,一个card实例只有一种可能的取值,但由于enum本质上只是整数,您不能确定它的值是否是有效的,在C#中,你可以这么写:
CardSuit invalid1 = (CardSuit) 9000;
CardSuit invalid2 = CardSuit.Club | CardSuit.Diamond;
另外,考虑下面的情形。如果您需要扩展一个enum:
enum Title { Mr, Mrs }
Title枚举可以工作地很好,但一段时间后,如果需要添加一个“Ms”值,那么每一个switch语句都面临一个潜在的bug。当然您可以尝试修复所有的代码,却难免会发生遗漏。
枚举可以很好地表达某些概念,但是却无法提供足够的编译器检查。F#中的Union类型可设定为一组有限的值:数据标签(data tag)。例如,考虑一个表示微软员工的Union:
type MicrosoftEmployee =
    | BillGates
    | SteveBalmer
    | Worker of string
    | Lead of string * MicrosoftEmployee list
如果有一个MicrosoftEmployee类型的实例,您就知道它必定是{BillGates,SteveBalmer,Worker,Lead}之一。另外,如果它是Worker,您可以知道有一个字符串与之关联,也许是他的名字。我们可以轻松地创建Union类型,而后使用模式匹配(下一小节)来匹配它们的值。
let myBoss = Lead("Yasir", [Worker("Chris"); Worker("Matteo"); Worker("Santosh")])

let printGreeting (emp : MicrosoftEmployee) =
    match emp with
    | BillGates   -> printfn "Hello, Bill"
    | SteveBalmer -> printfn "Hello, Steve"
    | Worker(name) | Lead(name, _)
                  -> printfn "Hello, %s" name
现在假设需要扩展Union类型:
type MicrosoftEmployee =
    | BillGates
    | SteveBalmer
    | Worker of string
    | Lead   of string * MicrosoftEmployee list
    | ChrisSmith
我们会看到一些编译器警告信息:

编译器检测到您没有匹配Union的每一个数据标签,发出了警告。像这样的检查会避免很多bug,要了解
更多的关于Union类型的信息,看这篇文章。
7. 模式匹配(Pattern Matching)
模式匹配看起来像是增强版的switch语句,允许您完成分支型控制流程。除了跟常数值进行比较外,还可以捕获新的值。比如在前面的例子中,我们在匹配Union数据标签时绑定了标识符“name”。
let printGreeting (emp : MicrosoftEmployee) =
    match emp with
    | BillGates   -> printfn "Hello, Bill"
    | SteveBalmer -> printfn "Hello, Steve"
    | Worker(name) | Lead(name, _)
                  -> printfn "Hello, %s" name
还可以对数据的“结构”进行匹配,比如对列表(list)进行匹配。(还记得吗,x :: y表示x为列表的一个元素,y是x之后的元素,而[]则是空列表。)
let listLength aList =
    match aList with
    | [] -> 0
    | a :: [] -> 1
    | a :: b :: [] -> 2
    | a :: b :: c :: [] -> 3
    | _ -> failwith "List is too big!"
在这个匹配的最后,我们使用了通配符“_”(下划线),它匹配任意值。如果aList变量包含多于三个的元素,最后的模式子句将执行,并抛出一个异常。模式匹配还可以我们执行任意表达式来确定模式是否匹配(如果表达式的值为false,则不匹配)。
let isOdd x =
    match x with
    | _ when x % 2 = 0 -> false
    | _ when x % 2 = 1 -> true
我们甚至可以使用动态类型测试进行匹配:
let getType (x : obj) =
    match x with
    | :? string -> "x is a string"
    | :? int -> "x is a int"
    | :? System.Exception -> "x is an exception"
    | :? _ -> "invalid type"
8. 记录类型(Records)
在声明包含若干个公有属性的类型时,记录类型是一种轻量级的方式。它的一个优势是,借助于类型推演系统,编译器可以通过值的声明得出适当的记录类型。
type Address = {Name : string; Address : string; Zip : int}
let whiteHouse = {Name = "The White House"; Address = "1600 Pennsylvania Avenue";
        Zip = 20500}
在上面的例子中,首先定义了“Address”类型,那么在声明它的实例时,无须显式地使用类型注解,编译器可根据字段(属性)的名称自行得出类型的信息。所以whiteHouse的类型为Address。
9. Forward Pipe Operator(|>)
|>操作符只是简单地定义为:
let (|>) x f = f x
其类型前面信息为:
'a -> ('a -> 'b) -> 'b
可以这么来理解:x的类型为'a,函数f接受'a类型的参数,返回类型为'b,操作符的结果就是将x传递给f后所求得的值。
还是来看个例子吧:
// Take a number, square it, then convert it to a string, then reverse that string

let square x         = x * x
let toStr (x : int)  = x.ToString()
let rev   (x : string) = new String(Array.rev (x.ToCharArray()))

// 32 -> 1024 -> "1024" -> "4201"
let result = rev (toStr (square 32))
上面的代码是很直白的,但语法看起来却不太好。我们所做的就是将一个运算的结果传给下一个运算。我们可以通过引入几个变量来改写代码为:
let step1 = square 32
let step2 = toStr step1
let step3 = rev step2
let result = step3
但是我们需要维护这几个临时变量。|>操作符接受一个值,将其“转交”给一个函数。这会大大地简化F#代码:
let result = 32 |> square |> toStr |> rev
10. 序列(Sequence,System.Collections.Generic.IEnumerator<_>)
序列(在F#中为seq)是 System.Collections.Generic.IEnumerator的别名,但它在F#中有另外的作用。不像列表和数组,序列可包含无穷个值。只有当前的值保存在内存中,一旦序列计算了下个值,当前的值就会被忘记(丢弃)。例如,下面的代码生成了一个包含所有整数的序列。
let allIntegers = Seq.init_infinite (fun i -> i)
11. 集合(Collections:Seq,List,Array)
在F#中,如果您想表示一个值的集合,至少有三个好的选择——数组、列表和序列,它们都有各自的优点。而且每种类型都有一系列的模块内置于F#库中。您可以使用VS的智能感知来探究这些方法,这里我们来看看最常用的那些:
iter。“iter”函数遍历集合的每一项。这与“foreach”循环是一致的。下面的代码打印列表的每一项:
List.iter (fun i -> printfn "Has element %d" i) [1 .. 10]
map。像我在上篇文章中所说的,map函数基于一个指定的函数对集合的值进行转换。下面的例子将数组的整数值转换为它们的字符串表示:
Array.map (fun (i : int) -> i.ToString()) [| 1 .. 10 |]
fold。“fold”函数接受一个集合,并将集合的值折叠为单个的值。像iter和map一样,它接受一个函数,将其应用于集合的每个元素,但它还接受另一个“accumulator”参数。fold函数基于上一次运算不断地累积accumulator参数的值。看下面的例子:
Seq.fold (fun acc i -> i + acc) 10 { 1 .. 10 }
该代码的功能是:以10为基数(acculator),累加序列中的每一项。
只有序列有fold方法,列表和数组则有fold_left和fold_right方法。它们的不同之处在于计算顺序的不同。
12. 可选值(Option Values)
基于函数式编程的特点,在F#中很难见到null值。但有些情况下,null值比未初始化变量更有意义。有时可选值则表示值未提供(可选值就像C#中的nullable类型)。
F#中的“可选类型(option type)”有两种状态:“Some”和“None”。在下面的记录类型Person中,中间的字段可能有值,也可能没有值。
type Person = { First : string; MI : string option; Last : string }
let billg    = {First = "Bill";  MI = Some("H"); Last = "Gates" }
let chrsmith = {First = "Chris"; MI = None;      Last = "Smith" }
13. 延迟求值(惰性值,Lazy Values,Microsoft.FSharp.Core.Lazy<_>)
延迟初始化表示一些值,它们在需要时才进行计算。F#拥有延迟求值特性。看下面的例子,“x”是一个整数,当对其进行求值时会打印“Computed”。
> let x = lazy (printfn "Computed."; 42);;
val x : Lazy<int>

> let listOfX = [x; x; x];;
val listOfX : Lazy<int> list

> x.Force();;
Computed.
val it : int = 42
可以看到,我们在调用“Force”方法时,对x进行求值,返回的值是42。您可以使用延迟初始化来避免不必要的计算。另外在构造递归值时,也很有用。例如,考虑一个Union值,它用来表示循环列表:
type InfiniteList =
| ListNode of int * InfiniteList

let rec circularList = ListNode(1, circularList)
“circularList”拥有对自身的引用(表示一个无限循环)。不使用延迟初始化的话,声明这样类型的值是不可能的。
游客
登录 | 注册 方可回帖
返回