Fsharp 图灵机






4.33/5 (3投票s)
受 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>]
主
|