Twenty six low-risk ways to use F# at work

https://swlaschin.gitbooks.io/fsharpforfunandprofit/content/posts/low-risk-ways-to-use-fsharp-at-work.html

Getting started

如果您使用的是 Visual Studio,那么您已经安装了 F#,所以您可以开始了!无需征求任何人的许可。

如果您使用的是 Mac 或 Linux,您将需要做一些工作,唉(MacLinux 的说明)。

有两种交互式使用 F# 的方法:(1) 直接在 F# 交互式窗口中键入,或 (2) 创建 F# 脚本文件 (.FSX),然后运行代码脚本。

在 Visual Studio 中使用 F# 交互式窗口:

  1. 使用 Menu > View > Other Windows > F# Interactive

  2. 使用 菜单 > 视图 > 其他窗口 > F# 交互窗口

  3. 输入一个表达式,并使用双分号(;;)告诉解释器你已经完成了。

例如:

let x = 1
let y = 2
x + y;;

就个人而言,我更喜欢创建一个脚本文件(文件 > 新建 > 文件,然后选择“F# 脚本”)并在其中键入代码,因为你可以获得自动完成和智能感知。

要运行一些代码片段,只需选中并右键单击,或简单地执行 Alt+Enter

使用外部库和 NuGet

大多数代码示例引用外部库,这些库应位于脚本目录下。

您可以显式下载或编译这些 DLL,但我认为从命令行使用 NuGet 更简单。

  1. 首先,你需要安装Chocolately(来自chocolatey.org)。

  2. 接下来使用安装 NuGet 命令行cinst nuget.commandline

  3. 最后,转到您的脚本文件的目录,并从命令行安装 NuGet 包。 例如,nuget install FSharp.Data -o Packages -ExcludeVersion 如您所见,在从脚本中使用 Nuget 包时,我更喜欢从中排除版本,这样我以后就可以在不破坏现有代码的情况下进行更新。

第 1 部分:使用 F# 以交互方式探索和开发

F#有价值的第一个领域是作为一个工具来交互式地探索.NET库。

以前,为了做到这一点,您可能已经创建了单元测试,然后使用调试器逐步执行它们以了解发生了什么。但是有了F#,你就不需要那么做了,直接运行代码就可以了。

让我们看一些例子。

1. 使用 F# 以交互方式探索 .NET

The code for this section is available on github.

当我编码时,我经常对 .NET 库的工作原理有一些疑问。

例如,以下是我最近使用 F# 交互式回答的一些问题:

  • 我的自定义 DateTime 格式字符串是否正确?

  • XML 序列化如何处理本地 DateTimes 与 UTC DateTimes?

  • GetEnvironmentVariable 区分大小写吗?

当然,所有这些问题都可以在 MSDN 文档中找到,但也可以通过运行一些简单的 F# 代码片段在几秒钟内得到解答,如下所示。

我的自定义 DateTime 格式字符串是否正确?

我想以自定义格式使用 24 小时制。我知道它是“h”,但它是大写还是小写的“h”?

open System
DateTime.Now.ToString("yyyy-MM-dd hh:mm")  // "2014-04-18 01:08"
DateTime.Now.ToString("yyyy-MM-dd HH:mm")  // "2014-04-18 13:09"

XML 序列化如何处理本地 DateTimes 与 UTC DateTimes?

XML 序列化究竟如何处理日期?我们来看一下

// TIP: sets the current directory to be same as the script directory
System.IO.Directory.SetCurrentDirectory (__SOURCE_DIRECTORY__)

open System

[<CLIMutable>] 
type DateSerTest = {Local:DateTime;Utc:DateTime}

let ser = new System.Xml.Serialization.XmlSerializer(typeof<DateSerTest>)

let testSerialization (dt:DateSerTest) = 
    let filename = "serialization.xml"
    use fs = new IO.FileStream(filename , IO.FileMode.Create)
    ser.Serialize(fs, o=dt)
    fs.Close()
    IO.File.ReadAllText(filename) |> printfn "%s"

let d = { 
    Local = DateTime.SpecifyKind(new DateTime(2014,7,4), DateTimeKind.Local)
    Utc = DateTime.SpecifyKind(new DateTime(2014,7,4), DateTimeKind.Utc)
    }

testSerialization d

输出是:

<DateSerTest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
  <Local>2014-07-04T00:00:00+01:00</Local>
  <Utc>2014-07-04T00:00:00Z</Utc>
</DateSerTest>

所以我可以看到它使用“Z”表示 UTC 时间。

GetEnvironmentVariable 区分大小写吗?

这可以用一个简单的片段来回答:

Environment.GetEnvironmentVariable "ProgramFiles" = 
    Environment.GetEnvironmentVariable "PROGRAMFILES"
// answer => true

因此答案是“不区分大小写”。

2. 使用 F# 以交互方式测试您自己的代码

The code for this section is available on github.

当然,您并不局限于使用 .NET 库。有时测试您自己的代码可能非常有用。

为此,只需引用 DLL,然后打开命名空间,如下所示。


// set the current directory to be same as the script directory
System.IO.Directory.SetCurrentDirectory (__SOURCE_DIRECTORY__)

// pass in the relative path to the DLL
#r @"bin\debug\myapp.dll"

// open the namespace
open MyApp

// do something
MyApp.DoSomething()

警告:在旧版本的 F# 中,打开对 DLL 的引用将锁定它,这样您就无法编译它!在这种情况下,在重新编译之前,请务必重置交互式会话以释放锁定。在较新版本的 F# 中,DLL 是隐式复制的,并且没有锁。

3. 使用 F# 以交互的方式使用 Web 服务

The code for this section is available on github.

如果您想使用 WebAPI 和 Owin 库,则无需创建可执行文件——您可以仅通过脚本来完成!

这涉及到一点设置,因为你将需要一些库的DLLs来使其工作。

因此,假设您已经设置了 NuGet 命令行(见上文),转到您的脚本目录,并通过以下方式安装自托管库 nuget install Microsoft.AspNet.WebApi.OwinSelfHost -o Packages -ExcludeVersion

一旦这些库到位,你可以使用下面的代码作为一个简单的WebAPI应用程序的骨架。

// sets the current directory to be same as the script directory
System.IO.Directory.SetCurrentDirectory (__SOURCE_DIRECTORY__)

// assumes nuget install Microsoft.AspNet.WebApi.OwinSelfHost has been run 
// so that assemblies are available under the current directory
#r @"Packages\Owin\lib\net40\Owin.dll"
#r @"Packages\Microsoft.Owin\lib\net40\Microsoft.Owin.dll"
#r @"Packages\Microsoft.Owin.Host.HttpListener\lib\net40\Microsoft.Owin.Host.HttpListener.dll"
#r @"Packages\Microsoft.Owin.Hosting\lib\net40\Microsoft.Owin.Hosting.dll"
#r @"Packages\Microsoft.AspNet.WebApi.Owin\lib\net45\System.Web.Http.Owin.dll"
#r @"Packages\Microsoft.AspNet.WebApi.Core\lib\net45\System.Web.Http.dll"
#r @"Packages\Microsoft.AspNet.WebApi.Client\lib\net45\System.Net.Http.Formatting.dll"
#r @"Packages\Newtonsoft.Json\lib\net40\Newtonsoft.Json.dll"
#r "System.Net.Http.dll"

open System
open Owin 
open Microsoft.Owin
open System.Web.Http 
open System.Web.Http.Dispatcher
open System.Net.Http.Formatting

module OwinSelfhostSample =

    /// a record to return
    [<CLIMutable>]
    type Greeting = { Text : string }

    /// A simple Controller
    type GreetingController() =
        inherit ApiController()

        // GET api/greeting
        member this.Get()  =
            {Text="Hello!"}

    /// Another Controller that parses URIs
    type ValuesController() =
        inherit ApiController()

        // GET api/values 
        member this.Get()  =
            ["value1";"value2"]

        // GET api/values/5 
        member this.Get id = 
            sprintf "id is %i" id 

        // POST api/values 
        member this.Post ([<FromBody>]value:string) = 
            ()

        // PUT api/values/5 
        member this.Put(id:int, [<FromBody>]value:string) =
            ()

        // DELETE api/values/5 
        member this.Delete(id:int) =
            () 

    /// A helper class to store routes, etc.
    type ApiRoute = { id : RouteParameter }

    /// IMPORTANT: When running interactively, the controllers will not be found with error:
    /// "No type was found that matches the controller named 'XXX'."
    /// The fix is to override the ControllerResolver to use the current assembly
    type ControllerResolver() =
        inherit DefaultHttpControllerTypeResolver()

        override this.GetControllerTypes (assembliesResolver:IAssembliesResolver) = 
            let t = typeof<System.Web.Http.Controllers.IHttpController>
            System.Reflection.Assembly.GetExecutingAssembly().GetTypes()
            |> Array.filter t.IsAssignableFrom
            :> Collections.Generic.ICollection<Type>    

    /// A class to manage the configuration
    type MyHttpConfiguration() as this =
        inherit HttpConfiguration()

        let configureRoutes() = 
            this.Routes.MapHttpRoute(
                name= "DefaultApi",
                routeTemplate= "api/{controller}/{id}",
                defaults= { id = RouteParameter.Optional }
                ) |> ignore

        let configureJsonSerialization() = 
            let jsonSettings = this.Formatters.JsonFormatter.SerializerSettings
            jsonSettings.Formatting <- Newtonsoft.Json.Formatting.Indented
            jsonSettings.ContractResolver <- 
                Newtonsoft.Json.Serialization.CamelCasePropertyNamesContractResolver()

        // Here is where the controllers are resolved
        let configureServices() = 
            this.Services.Replace(
                typeof<IHttpControllerTypeResolver>, 
                new ControllerResolver())

        do configureRoutes()
        do configureJsonSerialization()
        do configureServices()

    /// Create a startup class using the configuration    
    type Startup() = 

        // This code configures Web API. The Startup class is specified as a type
        // parameter in the WebApp.Start method.
        member this.Configuration (appBuilder:IAppBuilder) = 
            // Configure Web API for self-host. 
            let config = new MyHttpConfiguration() 
            appBuilder.UseWebApi(config) |> ignore


// Start OWIN host 
do 
    // Create server
    let baseAddress = "http://localhost:9000/" 
    use app = Microsoft.Owin.Hosting.WebApp.Start<OwinSelfhostSample.Startup>(url=baseAddress) 

    // Create client and make some requests to the api
    use client = new System.Net.Http.HttpClient() 

    let showResponse query = 
        let response = client.GetAsync(baseAddress + query).Result 
        Console.WriteLine(response) 
        Console.WriteLine(response.Content.ReadAsStringAsync().Result) 

    showResponse "api/greeting"
    showResponse "api/values"
    showResponse "api/values/42"

    // for standalone scripts, pause so that you can test via your browser as well
    Console.ReadLine() |> ignore

以下是输出:

StatusCode: 200, ReasonPhrase: 'OK', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
  Date: Fri, 18 Apr 2014 22:29:04 GMT
  Server: Microsoft-HTTPAPI/2.0
  Content-Length: 24
  Content-Type: application/json; charset=utf-8
}
{
  "text": "Hello!"
}
StatusCode: 200, ReasonPhrase: 'OK', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
  Date: Fri, 18 Apr 2014 22:29:04 GMT
  Server: Microsoft-HTTPAPI/2.0
  Content-Length: 29
  Content-Type: application/json; charset=utf-8
}
[
  "value1",
  "value2"
]
StatusCode: 200, ReasonPhrase: 'OK', Version: 1.1, Content: System.Net.Http.StreamContent, Headers:
{
  Date: Fri, 18 Apr 2014 22:29:04 GMT
  Server: Microsoft-HTTPAPI/2.0
  Content-Length: 10
  Content-Type: application/json; charset=utf-8
}
"id is 42"

这个例子只是为了证明你可以使用OWIN和WebApi库 "开箱即用"。

对于 F# 更友好的 Web 框架,请查看 Suave or WebSharper. 有很多 more webby stuff at fsharp.org.

4. 使用 F# 以交互方式来使用 UI

本节的代码可在 github 上找到 available on github.

F# interactive 的另一个用途是在 UI 运行时使用它们——实时!

下面是一个交互式开发 WinForms 屏幕的示例。

open System.Windows.Forms 
open System.Drawing

let form = new Form(Width= 400, Height = 300, Visible = true, Text = "Hello World") 
form.TopMost <- true
form.Click.Add (fun _ -> 
    form.Text <- sprintf "form clicked at %i" DateTime.Now.Ticks)
form.Show()

这是窗口:

这是单击后的窗口,标题栏已更改:

现在让我们添加一个 FlowLayoutPanel 和一个按钮。

let panel = new FlowLayoutPanel()
form.Controls.Add(panel)
panel.Dock = DockStyle.Fill 
panel.WrapContents <- false 

let greenButton = new Button()
greenButton.Text <- "Make the background color green" 
greenButton.Click.Add (fun _-> form.BackColor <- Color.LightGreen)
panel.Controls.Add(greenButton)

现在的窗口是这样的:

但是按钮太小——我们需要将 AutoSize 设置为 true。

greenButton.AutoSize <- true

这样就更好了!

让我们再添加一个黄色按钮:

let yellowButton = new Button()
yellowButton.Text <- "Make me yellow" 
yellowButton.AutoSize <- true
yellowButton.Click.Add (fun _-> form.BackColor <- Color.Yellow)
panel.Controls.Add(yellowButton)

但是按钮被切断了,所以让我们改变排列方向:

panel.FlowDirection <- FlowDirection.TopDown

但是现在黄色按钮和绿色按钮的宽度不一样,我们可以用 Dock 解决这个问题:

yellowButton.Dock <- DockStyle.Fill

如您所见,以这种方式交互式地使用布局真的很容易。一旦您对布局逻辑感到满意,您就可以将代码转换回 C# 以用于您的实际应用程序。

这个例子是特定于 WinForms 的。当然,对于其他 UI 框架,逻辑会有所不同。


因此,这就是前四项建议。我们还没说完呢! 下一篇文章将介绍使用 F# 进行开发和编写 devops 脚本。

Last updated