65.9K
CodeProject 正在变化。 阅读更多。
Home

Fsharp 图灵机

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.33/5 (3投票s)

2012年9月17日

CPOL

10分钟阅读

viewsIcon

13996

downloadIcon

268

受 Langton 蚁群模式启发的 F# 图灵机(两状态)。

 

引言

这是一个用 F# 编写的双状态图灵机,灵感来自兰顿的蚂蚁模式。我很高兴分享我的工作,并向您介绍示例,讨论 F# 语言结构。

背景

有三条规则,它们非常简单:蚂蚁一次移动一步,移动时会翻转方块的颜色。当从白色方块移动时,它会向右移动。当从黑色方块移动时,它会向左移动。

黑白屏幕不够令人兴奋,我在最后一刻改变了主意,方块被花朵取代,因此蚂蚁变成了虫子。(你知道吗?你的“剪贴板”里有这个虫子,我花了好几个小时才找到它。)

前言

我练习 F# 已经有一段时间了,这项工作是为了让我这个周末投入其中。此外,这篇文章讨论的是所使用的语言结构,而不是工作本身。如果我有任何“初学者”的错误,请随时纠正我。

乐趣从这里开始

我想向您介绍一个经典的“Hello World”C# 示例,并将其与 F# 版本进行比较,以向您展示 F# 为何有趣以及 F# 如何带来不同。

// fsharp
open  System
let  msg = "Hello World!"
Console.WriteLine msg
// CSharp
using  System;
namespace  HelloWorld
{
    class Program
    {
        private static String msg = "Hello World!";
        static void Main(string[] args)
        {
            Console.WriteLine(msg);
        }
    }
}

您知道其中的区别,它不需要任何解释,编程从未如此简单和清晰。

使用 let

在 F# 中要学习的第一件重要事情是“let”绑定。关键字“let”用于将值或对象绑定到标识符,因此程序中的任何地方都可以访问该引用。同一个标识符可以在子块内使用,并且在当前上下文中可用,并超出父上下文中的标识符范围。

> let index = 0;;
val index : int = 0

这里展示的所有代码都是在 F# 交互式窗口 (FSI) 中编写和操作的,您可以将其视为 vs2010 中的交互式窗口,并且任何块的末尾都必须以“;;”终止,以便 FSI 评估块的值,但是,在 F# 中,语句或行不必以“;”终止。

请注意,在 index 声明中没有指定类型,FSI 将 index 推断为值,类型为 int。默认情况下,F# 中的每个标识符都是不可变的,这意味着一旦绑定,值就不能更改。如果将值分配给不可变标识符,编译器将抛出错误,这就是为什么它不被称为变量。

> index <- 5;;
  index <- 5;;
  ^^^^^^^^^^
stdin(3,1): error FS0027: This value is not mutable

在我们日常编程的大多数情况下,变量和对象都在改变它们的状态。要修改标识符的值,它必须显式标记为可变,编译器会很高兴地为可变标识符赋值。

> let mutable pointer = 0;;
val mutable pointer : int = 0

要绑定值,必须使用相等 (=) 运算符,并使用赋值运算符 ( <- ) 为标识符赋值。

> let mutable pointer = 0
pointer <- 5
val mutable pointer : int = 5

函数

函数声明与标识符相同,它们接受参数进行操作。F# 支持函数的部分应用和函数柯里化;因此,部分函数可以用作一流参数。这是一种更强大的编写更好代码和提高生产力的方式。让我们声明一个简单的函数,它接受输入“x”并将值加一。

> let increment x = x + 1;;
val increment : int - > int

你看到了吗?参数类型和返回类型都没有提到。编译器将参数类型推断为整数,这是如何做到的?答案是函数体将整数 1 加到 x,所以 x 是整数的可能性更大,因此它将 x 的类型标记为整数。默认情况下,块或函数的最后一个语句被评估为块的值,其类型为返回类型。由于我们的函数体评估为整数并且是块的最后一行,因此返回类型是整数。

让我们改变代码,看看类型是如何推断的,

> let increment x = x + 1.0;;
val increment : float - > float

现在函数体将浮点数加到 x,因此参数类型和返回类型都是浮点数。FSI 对 increment 的概念是它接受一个浮点数并返回 (->) 一个浮点数 (float -> float)。

鉴于此,让我们看看部分应用是如何可能的。这是一个接受两个参数、将它们相乘并返回结果的函数。

> let multiply x y = x * y;;
val multiply : int -> int -> int

让我们仔细看看 FSI 对 multiply 函数的概念,它接受一个函数 (int -> int) 作为参数并返回一个整数,

(int -> int) -> int

这样做是安全的,multiply 函数部分应用于 2,并作为 double 可用,这有意义吗?

> let double = multiply 2;;
val double : (int -> int)
> double 5;;
val it : int = 10

我们的情况很简单,它只接受一个方块,改变其状态,不返回任何东西。请注意,参数类型在括号中明确指定,如果没有使用括号,则表示函数的返回类型。

> let flipTile (tile:Tile) = tile.State <- State.Black;;
val flipTile : Tile - > unit

模式匹配是做出决策、控制执行流以及做更多事情的强大方式。有不同的方法来匹配不同的类型,但对于我们的需要来说,它是最简单的。

方块的当前状态是已知的,并且必须预测下一步移动。我们将在本文后面讨论类型,现在将 State 和 Move 视为 Enum。

> let getNextMove (currentState:State) =
    match currentState with
    | White -> Right
    | _ -> Left;;
val getNextMove : State -> Move

这更像我们案例中的 if then else / case 语句,如果它匹配 State.White 则返回 Move.Right,否则返回 Move.Left,但请记住还有更多。

有一种不同的方法可以简化我们的生活。请注意,FSI 的概念保持不变。

> let getNextMove = function
    | White -> Right
    | _ -> Left;;
val getNextMove : State -> Move

数组

F# 还有其他不同的数据结构,如 List 和 Sequences (Seq)。它们各自强大,例如 Seq 可以用来创建无限长度的序列,并且几乎可以以最少的内存处理它,Seq 是惰性加载的,在任何时候迭代中的当前元素都是活动的。

这项工作首选数组,只是因为方块可以通过索引作为句柄访问。我们可以通过在“[| |]”符号中指定元素来构造一个简单的整数数组。

> let nums = [| 1; 2; 3; 4; 5; |];;
val nums : int [] = [|1; 2; 3; 4; 5|]

还有其他初始化方法,这里有几个

> let nums = [| 1..1..10|];;
val nums : int [] = [|1; 2; 3; 4; 5; 6; 7; 8; 9; 10|]
      
> let nums = [| 2..2..20|];;
val nums : int [] = [|2; 4; 6; 8; 10; 12; 14; 16; 18; 20|]

最后是方块的初始化方式,

>  let tiles = Array.init 3 (fun i -> new Tile(i));;
val tiles : Tile [] = [|FSI_0004+Tile; FSI_0004+Tile; FSI_0004+Tile|]

Array.init 接受要生成的项目数量和用于创建项目的生成器函数。

Types

F# 都是关于类型和类型推断的;如果您正在阅读这篇文章,那么您也一定对类型提供程序感兴趣,我建议您观看 Daniel Spiewak 关于 类型推断原理 的演讲

让我在这里简要介绍一下类型定义。

以下是创建类型的方式之一的语法,以下使用带有属性和方法的显式构造函数。

type typename (param:type) =

let mutable member = something

member alias.MemberName

with get() = value

and set(value) = member <- value

member alias.MethodName() = [some function]

type Tile (i:int) =
let mutable state = White
member x.State
with get() = state
and set(value) = state <- value
member x.Reset() =
x.State <- White
>
type Tile =
  class
    new : i:int -> Tile
    member Reset : unit -> unit
    member State : State
    member State : State with set
  end

继承

如前所述,F# 支持面向对象编程概念。类型可以从其他类型继承并覆盖其成员,这分别可以通过“inherits”和“override”关键字来完成。

对于这项工作,UI 是用 WPF 构建的,属性中的任何更改都必须通知 UI。我们有一个实现通知机制“ObservableObject”的类型,我们的 Tile 类型将继承自它。

type Tile (i:int) =
inherit ObservableObject()
      
let mutable state = White
member x.State
with get() = state
and set(value) = state <- value ; x.Notify x "State"
member x.Reset() =
x.State <- White
>
type Tile =
  class
    inherit ObservableObject
    new : i:int -> Tile
    member Reset : unit -> unit
    member State : State
    member State : State with set
  end

事件和接口

事件和接口之间没有关系或意图将它们放在一起,但前一个代码块中提到的 ObservableObject 同时使用了它们,因此可以作为一个单独的示例。接口实现类似于类型中的成员和方法定义,语法是:

interface IInterfaceName with

member x.InterfaceMember = someValue

meber x.InterfaceMethod() = somFunction

事件分三步实现,首先是定义事件签名,其次是发布事件,另一方面将事件暴露给外部世界以便钩住它,最后一步是在需要时触发事件。

type ObservableObject () =
    let propertyChangedEvent = new Event<_,_>()
    interface INotifyPropertyChanged with
        member x.PropertyChanged = propertyChangedEvent.Publish
    member x.Notify s n = propertyChangedEvent.Trigger (s, PropertyChangedEventArgs n)
>
type ObservableObject =
  class
    interface System.ComponentModel.INotifyPropertyChanged
    new : unit -> ObservableObject
    member Notify : s:'a -> n:string -> unit
  end

就是这样,您现在可以体验虫子的移动了。

托管窗口

UI 以 WPF 窗口呈现,花朵和虫子是模板,并根据方块的状态通过触发器更新,假设读者对 WPF 有足够的了解。以下是 F# 中如何托管窗口的。

从解决方案资源管理器添加一个 xml 文件,并将扩展名更新为 .Xaml

将文件标记为资源,并将 xaml 内容粘贴到文件中。

let main =
    let app = Application()
    let window = Application.LoadComponent(new
Uri ("TheForgottonAnt;component/MainWindow.xaml", System.UriKind.Relative)) :?> Window
    app.Run window |> ignore
[<STAThread>]

快乐编程!!! 

© . All rights reserved.