方便的 F# 片段 [关闭]

Posted

技术标签:

【中文标题】方便的 F# 片段 [关闭]【英文标题】:Handy F# snippets [closed] 【发布时间】:2010-10-24 09:47:06 【问题描述】:

已经有twoquestions关于F#/functional sn-ps。

但我在这里寻找的是有用 sn-ps,可重复使用的小“帮助”功能。或者你永远记不住的晦涩但漂亮的模式。

类似:

open System.IO

let rec visitor dir filter= 
    seq  yield! Directory.GetFiles(dir, filter)
          for subdir in Directory.GetDirectories(dir) do 
              yield! visitor subdir filter 

我想让它成为一种方便的参考页面。因此,不会有正确的答案,但希望有很多好的答案。

编辑 Tomas Petricek 专门为 F# sn-ps http://fssnip.net/ 创建了一个站点。

【问题讨论】:

请将其设为社区 wiki。 完成,我认为从一个普通问题开始可能会为一些初步答案提供动力。 【参考方案1】:

Perl 风格正则表达式匹配

let (=~) input pattern =
    System.Text.RegularExpressions.Regex.IsMatch(input, pattern)

它允许您使用let test = "monkey" =~ "monk.+" 表示法匹配文本。

【讨论】:

【参考方案2】:

中缀运算符

我是从 http://sandersn.com/blog//index.php/2009/10/22/infix-function-trick-for-f 那里得到的,请访问该页面了解更多详情。

如果您了解 Haskell,您可能会发现自己在 F# 中缺少中缀糖:

// standard Haskell call has function first, then args just like F#. So obviously
// here there is a function that takes two strings: string -> string -> string 
startsWith "kevin" "k"

//Haskell infix operator via backQuotes. Sometimes makes a function read better.
"kevin" `startsWith` "K" 

虽然 F# 没有真正的“中缀”运算符,但同样的事情可以通过管道和“后管道”几乎同样优雅地完成(谁知道这样的事情??)

// F# 'infix' trick via pipelines
"kevin" |> startsWith <| "K"

【讨论】:

+1:我不知道为什么,但这让我很兴奋 :) @Juliet 我也是,但我想我知道为什么。想起一张图。 (不会在这里发帖,这是 NSFW)【参考方案3】:

多行字符串

这很微不足道,但它似乎是 F# 字符串的一个鲜为人知的特性。

let sql = "select a,b,c \
           from table \
           where a = 1"

这会产生:

val sql : string = "select a,b,c from table where a = 1"

当 F# 编译器在字符串文字中看到反斜杠后跟回车时,它将删除从反斜杠到下一行第一个非空格字符的所有内容。这允许您拥有排列的多行字符串文字,而无需使用一堆字符串连接。

【讨论】:

只是补充一点,C# 样式 @"string" 也适用于 F# 中的多行! 未来仅供参考。你不再需要反斜杠了。 @Gagege - 感谢您的提示,但您能否具体说明您来自哪个未来?我刚刚在 F# 3.1,VS 2013 中尝试过,如果您希望在此版本中从结果字符串中去除每行开头的空格,您仍然需要斜杠。 @Gagege 不带反斜杠的多行字符串文字不会修剪字符串中的换行符和前导空格(在 F# 4.0 中测试)【参考方案4】:

通用记忆,由the man本人提供

let memoize f = 
  let cache = System.Collections.Generic.Dictionary<_,_>(HashIdentity.Structural)
  fun x ->
    let ok, res = cache.TryGetValue(x)
    if ok then res
    else let res = f x
         cache.[x] <- res
         res

使用这个,你可以像这样做一个缓存阅读器:

let cachedReader = memoize reader

【讨论】:

您应该将HashIdentity.Structural 传递给Dictionary,否则它将在键上使用默认的.NET 引用相等而不是F# 的结构相等。 .NET 喜欢通过引用比较值,而 F# 在结构上比较值(即根据它们的内容)。所以两个数组 [|2|] 和 [|2|] 根据 .NET 不相等,但根据 F# 相等。如果这些值在您的代码中以“x”的形式出现,则会对 F# 程序员产生意想不到的结果。当然,这在我的书中有所描述。 @Jon Harrop dict 运算符会这样做吗? @Ryan Riley:如果你使用dict [[|1|], 2; [|1|], 4],那么你会得到一个键为[|1|] 的绑定,这表明它确实使用了结构散列,是的。 我会增强这个记忆功能,让它更安全。如果已将可区分联合设置为具有空编译表示,如果您曾经将该案例作为键插入,它将在运行时崩溃。因此,我会在持久化之前将密钥包装在选项类型中。【参考方案5】:

对文本文件的简单读写

这些是微不足道的,但使文件访问变得可管道化:

open System.IO
let fileread f = File.ReadAllText(f)
let filewrite f s = File.WriteAllText(f, s)
let filereadlines f = File.ReadAllLines(f)
let filewritelines f ar = File.WriteAllLines(f, ar)

所以

let replace f (r:string) (s:string) = s.Replace(f, r)

"C:\\Test.txt" |>
    fileread |>
    replace "teh" "the" |>
    filewrite "C:\\Test.txt"

并将其与问题中引用的访问者相结合:

let filereplace find repl path = 
    path |> fileread |> replace find repl |> filewrite path

let recurseReplace root filter find repl = 
    visitor root filter |> Seq.iter (filereplace find repl)

更新如果您希望能够读取“锁定”文件(例如已在 Excel 中打开的 csv 文件...),请稍作改进:

let safereadall f = 
   use fs = new FileStream(f, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)
   use sr = new StreamReader(fs, System.Text.Encoding.Default)
   sr.ReadToEnd()

let split sep (s:string) = System.Text.RegularExpressions.Regex.Split(s, sep)

let fileread f = safereadall f
let filereadlines f = f |> safereadall |> split System.Environment.NewLine  

【讨论】:

您可以通过管道传递 File.ReadAllLines,它返回一个数组...例如 File.ReadAllLines(file) |> Array.map print_line 我想看看这些的一些用法示例,因为我是新手...专门针对开头"make file access pipeable:" 对此感到困惑,因为所有这些文件方法管道都很好,没有别名。例如 "C:\\somefile.txt" |> File.ReadAllText @piers7,只有一个参数。将单参数混叠会使事情看起来不那么不平衡(对我来说)。【参考方案6】:

对于需要检查 null 的性能密集型内容

let inline isNull o = System.Object.ReferenceEquals(o, null)
if isNull o then ... else ...

比大约快 20 倍

if o = null then ... else ...

【讨论】:

另请注意,如果您使用泛型,o = null 需要 Equality 约束 天啊!为什么差别这么大? @SargeBorsch 因为前者仅转换为参考比较,而其他调用 FSharp.Core.LanguagePrimitives.HashCompare.GenericEqualityIntrinsic,这是更多代码。 注意,F# 4.0, the isNull 现在是一个标准的内联运算符,如果您不使用它,FSharpLint 会抱怨。【参考方案7】:

Active Patterns,又名“Banana Splits”,是一种非常方便的构造,可以让一个匹配多个正则表达式模式。这很像AWK,但没有DFA 的高性能,因为模式是按顺序匹配的,直到成功为止。

#light
open System
open System.Text.RegularExpressions

let (|Test|_|) pat s =
    if (new Regex(pat)).IsMatch(s)
    then Some()
    else None

let (|Match|_|) pat s =
    let opt = RegexOptions.None
    let re = new Regex(pat,opt)
    let m = re.Match(s)
    if m.Success
    then Some(m.Groups)
    else None

一些使用示例:

let HasIndefiniteArticle = function
        | Test "(?: |^)(a|an)(?: |$)" _ -> true
        | _ -> false

type Ast =
    | IntVal of string * int
    | StringVal of string * string
    | LineNo of int
    | Goto of int

let Parse = function
    | Match "^LET\s+([A-Z])\s*=\s*(\d+)$" g ->
        IntVal( g.[1].Value, Int32.Parse(g.[2].Value) )
    | Match "^LET\s+([A-Z]\$)\s*=\s*(.*)$" g ->
        StringVal( g.[1].Value, g.[2].Value )
    | Match "^(\d+)\s*:$" g ->
        LineNo( Int32.Parse(g.[1].Value) )
    | Match "^GOTO \s*(\d+)$" g ->
        Goto( Int32.Parse(g.[1].Value) )
    | s -> failwithf "Unexpected statement: %s" s

【讨论】:

【参考方案8】:

也许是单子

type maybeBuilder() =
    member this.Bind(v, f) =
        match v with
        | None -> None
        | Some(x) -> f x
    member this.Delay(f) = f()
    member this.Return(v) = Some v

let maybe = maybeBuilder()

这里是 monads 的简短介绍,适合初学者。

【讨论】:

我在github.com/panesofglass/FSharp.Monad 有一个完整的库;其中许多来自 Matthew Podwysocki 的系列。【参考方案9】:

选项合并运算符

我想要一个 defaultArg 函数版本,它的语法更接近 C# 空合并运算符 ??。这让我可以使用非常简洁的语法从选项中获取值,同时提供默认值。

/// Option-coalescing operator - this is like the C# ?? operator, but works with 
/// the Option type.
/// Warning: Unlike the C# ?? operator, the second parameter will always be 
/// evaluated.
/// Example: let foo = someOption |? default
let inline (|?) value defaultValue =
    defaultArg value defaultValue

/// Option-coalescing operator with delayed evaluation. The other version of 
/// this operator always evaluates the default value expression. If you only 
/// want to create the default value when needed, use this operator and pass
/// in a function that creates the default.
/// Example: let foo = someOption |?! (fun () -> new Default())
let inline (|?!) value f =
    match value with Some x -> x | None -> f()

【讨论】:

整洁 - 延迟评估版本的另一个选项是使用 Lazy&lt;'a&gt; 作为第二个参数而不是 unit -&gt; 'a,然后示例看起来像 someOption |?! lazy(new Default()) @Stephen - 好点。我其实更喜欢这样。【参考方案10】:

'Unitize' 一个不处理单位的函数 使用FloatWithMeasure函数http://msdn.microsoft.com/en-us/library/ee806527(VS.100).aspx。

let unitize (f:float -> float) (v:float<'u>) =
  LanguagePrimitives.FloatWithMeasure<'u> (f (float v))

例子:

[<Measure>] type m
[<Measure>] type kg

let unitize (f:float -> float) (v:float<'u>) =
  LanguagePrimitives.FloatWithMeasure<'u> (f (float v))

//this function doesn't take units
let badinc a = a + 1.

//this one does!
let goodinc v = unitize badinc v

goodinc 3.<m>
goodinc 3.<kg>

旧版本

let unitize (f:float -> float) (v:float<'u>) =
  let unit = box 1. :?> float<'u>
  unit * (f (v/unit))

向kvb致敬

【讨论】:

【参考方案11】:

比例/比率函数构建器

同样,微不足道,但很方便。

//returns a function which will convert from a1-a2 range to b1-b2 range
let scale (a1:float<'u>, a2:float<'u>) (b1:float<'v>,b2:float<'v>) = 
    let m = (b2 - b1)/(a2 - a1) //gradient of line (evaluated once only..)
    (fun a -> b1 + m * (a - a1))

例子:

[<Measure>] type m
[<Measure>] type px

let screenSize = (0.<px>, 300.<px>)
let displayRange = (100.<m>, 200.<m>)
let scaleToScreen = scale displayRange screenSize

scaleToScreen 120.<m> //-> 60.<px>

【讨论】:

【参考方案12】:

转置列表(见于Jomo Fisher's blog)

///Given list of 'rows', returns list of 'columns' 
let rec transpose lst =
    match lst with
    | (_::_)::_ -> List.map List.head lst :: transpose (List.map List.tail lst)
    | _         -> []

transpose [[1;2;3];[4;5;6];[7;8;9]] // returns [[1;4;7];[2;5;8];[3;6;9]]

这是一个尾递归版本,它(根据我的粗略分析)稍微慢一些,但当内部列表长于 10000 个元素时(在我的机器上),它的优点是不会引发堆栈溢出:

let transposeTR lst =
  let rec inner acc lst = 
    match lst with
    | (_::_)::_ -> inner (List.map List.head lst :: acc) (List.map List.tail lst)
    | _         -> List.rev acc
  inner [] lst

如果我够聪明,我会尝试用异步并行化它...

【讨论】:

【参考方案13】:

F# 映射 C# 字典

(我知道,我知道,System.Collections.Generic.Dictionary 并不是真正的“C#”字典)

C# 到 F#

(dic :> seq<_>)                        //cast to seq of KeyValuePair
    |> Seq.map (|KeyValue|)            //convert KeyValuePairs to tuples
    |> Map.ofSeq                       //convert to Map

(来自 Brian,here,Mauricio 在下面的评论中提出了改进。(|KeyValue|) 是匹配 KeyValuePair 的活动模式 - 来自 FSharp.Core - 相当于 (fun kvp -&gt; kvp.Key, kvp.Value)

有趣的选择

要获得所有不可变的优点,但使用 Dictionary 的 O(1) 查找速度,您可以使用 dict 运算符,它返回一个不可变的 IDictionary(请参阅 this question)。

我目前看不到使用此方法直接转换字典的方法,除了

(dic :> seq<_>)                        //cast to seq of KeyValuePair
    |> (fun kvp -> kvp.Key, kvp.Value) //convert KeyValuePairs to tuples
    |> dict                            //convert to immutable IDictionary

F# 到 C#

let dic = Dictionary()
map |> Map.iter (fun k t -> dic.Add(k, t))
dic

这里奇怪的是,FSI 会将类型报告为(例如):

val it : Dictionary<string,int> = dict [("a",1);("b",2)]

但如果您将 dict [("a",1);("b",2)] 反馈回来,FSI 会报告

IDictionary<string,int> = seq[[a,1] Key = "a"; Value = 1;  ...

【讨论】:

我认为您在将 KeyValues 转换为元组时缺少 Seq.map。另外,您可以使用(|KeyValue|) 代替fun kvp -&gt; kvp.Key,kvp.Value @Mauricio,很好的发现,(|KeyValue|) 的绝妙技巧 - 这几乎值得它自己的 sn-p!【参考方案14】:

树排​​序/将树展平为列表

我有以下二叉树:

             ___ 77 _
            /        \
   ______ 47 __       99
  /            \
21 _          54
    \        /  \
      43    53  74
     /
    39
   /
  32

表示如下:

type 'a tree =
    | Node of 'a tree * 'a * 'a tree
    | Nil

let myTree =
    Node
      (Node
         (Node (Nil,21,Node (Node (Node (Nil,32,Nil),39,Nil),43,Nil)),47,
          Node (Node (Nil,53,Nil),54,Node (Nil,74,Nil))),77,Node (Nil,99,Nil))

扁平化树的简单方法是:

let rec flatten = function
    | Nil -> []
    | Node(l, a, r) -> flatten l @ a::flatten r

这不是尾递归,我相信@ 运算符会导致它在不平衡二叉树的情况下为 O(n log n) 或 O(n^2)。稍作调整,我想出了这个尾递归 O(n) 版本:

let flatten2 t =
    let rec loop acc c = function
        | Nil -> c acc
        | Node(l, a, r) ->
            loop acc (fun acc' -> loop (a::acc') c l) r
    loop [] (fun x -> x) t

这是 fsi 中的输出:

> flatten2 myTree;;
val it : int list = [21; 32; 39; 43; 47; 53; 54; 74; 77; 99]

【讨论】:

@Benjol:我不确定像 flatten2 这样的例子是支持还是反对延续传递风格的论据;) @Benjol 将尾递归版本视为将数据存储在闭包中而不是堆栈中。如果您查看“(fun acc' -> loop (a::acc') cl)”,则只有 acc' 被传递给函数,因此 F# 必须以某种方式保存 a、c、l 以供将来评估函数时使用. 您可能会发现使用延续传递样式或显式累积堆栈或父节点以进行左递归更容易在树上编写折叠。然后在折叠方面写flattenfold cons [] xs 我更喜欢方案版本(apply append lst1 lst2 lst3)。虽然不是递归的。【参考方案15】:

LINQ-to-XML 帮助程序

namespace System.Xml.Linq

// hide warning about op_Explicit
#nowarn "77"

[<AutoOpen>]
module XmlUtils =

    /// Converts a string to an XName.
    let xn = XName.op_Implicit
    /// Converts a string to an XNamespace.
    let xmlns = XNamespace.op_Implicit

    /// Gets the string value of any XObject subclass that has a Value property.
    let inline xstr (x : ^a when ^a :> XObject) =
        (^a : (member get_Value : unit -> string) x)

    /// Gets a strongly-typed value from any XObject subclass, provided that
    /// an explicit conversion to the output type has been defined.
    /// (Many explicit conversions are defined on XElement and XAttribute)
    /// Example: let value:int = xval foo
    let inline xval (x : ^a when ^a :> XObject) : ^b = 
        ((^a or ^b) : (static member op_Explicit : ^a -> ^b) x) 

    /// Dynamic lookup operator for getting an attribute value from an XElement.
    /// Returns a string option, set to None if the attribute was not present.
    /// Example: let value = foo?href
    /// Example with default: let value = defaultArg foo?Name "<Unknown>"
    let (?) (el:XElement) (name:string) =
        match el.Attribute(xn name) with
        | null -> None
        | att  -> Some(att.Value)

    /// Dynamic operator for setting an attribute on an XElement.
    /// Example: foo?href <- "http://www.foo.com/"
    let (?<-) (el:XElement) (name:string) (value:obj) =
        el.SetAttributeValue(xn name, value)

【讨论】:

感谢这些。我已经看到 Tomas 抛出 ? 类型的运算符,但我不明白的是 name-as-code 是如何神奇地转换为 name-as-string 的(为什么你不必这样做 @987654322 @?)。中间的?&lt;-“分崩离析”究竟有什么魔力? 我的无知暴露无遗 @Benjol - 这是一个编译器技巧。 F# 编译器将? 运算符的定义转换为一个名为op_Dynamic 的静态类方法,该方法接受一个字符串参数。然后它将? 运算符的使用转换为对该方法的调用,其中问号后面的部分作为字符串参数。所以在运行时它都是静态类型的而不是动态的,它只是提供了一些很好的简洁语法,你可以定义它的行为。与?&lt;- 运算符的原理相同。【参考方案16】:

好的,这与sn-ps无关,但我一直忘记这个:

如果您在交互式窗口中,则按 F7 以跳回代码窗口(无需取消选择刚刚运行的代码...)

从代码窗口转到 F# 窗口(以及打开 F# 窗口)是 Ctrl Alt F

(除非 CodeRush 偷了你的绑定...)

【讨论】:

FWIW 您可以更改 Ctrl+Alt+F CodeRush 绑定,使其仅在 DXCore 支持的语言中运行(即不支持 F#)。要做到这一点:找到“DevExpress\CodeRush\Options”菜单...选择左侧的 IDE\Shortcuts ...找到 Navigation\Ctrl+Alt+F 快捷方式。突出显示它,然后勾选右侧的“Language\Active Language Supported”上下文项。单击确定,此快捷方式应该开始按您想要的方式工作。【参考方案17】:

数组的加权和

根据 [k-array] 权重计算 [k-array of n-arrays] 数字的加权 [n-array] 总和

(复制自this question 和kvb 的answer)

给定这些数组

let weights = [|0.6;0.3;0.1|]

let arrs = [| [|0.0453;0.065345;0.07566;1.562;356.6|] ; 
           [|0.0873;0.075565;0.07666;1.562222;3.66|] ; 
           [|0.06753;0.075675;0.04566;1.452;3.4556|] |]

我们想要一个加权和(按列),因为数组的两个维度都是可变的。

Array.map2 (fun w -> Array.map ((*) w)) weights arrs 
|> Array.reduce (Array.map2 (+))

第一行:将第一个 Array.map2 函数部分应用于权重会产生一个新函数 (Array.map ((*) weight),该函数会(针对每个权重)应用于arr。

第二行:Array.reduce 类似于折叠,只是它从第二个值开始并使用第一个作为初始“状态”。在这种情况下,每个值都是我们数组的“行”。因此,在前两行应用 Array.map2 (+) 意味着我们将前两个数组相加,这给我们留下了一个新数组,然后我们 (Array.reduce) 再次将其与下一个相加(在这种情况下是最后一个)数组。

结果:

[|0.060123; 0.069444; 0.07296; 1.5510666; 215.40356|]

【讨论】:

这让我大吃一惊,因为我从没想过可以映射两个不同的列表。【参考方案18】:

性能测试

(找到 here 并更新为最新版本的 F#)

open System
open System.Diagnostics 
module PerformanceTesting =
    let Time func =
        let stopwatch = new Stopwatch()
        stopwatch.Start()
        func()
        stopwatch.Stop()
        stopwatch.Elapsed.TotalMilliseconds

    let GetAverageTime timesToRun func = 
        Seq.initInfinite (fun _ -> (Time func))
        |> Seq.take timesToRun
        |> Seq.average

    let TimeOperation timesToRun =
        GC.Collect()
        GetAverageTime timesToRun

    let TimeOperations funcsWithName =
        let randomizer = new Random(int DateTime.Now.Ticks)
        funcsWithName
        |> Seq.sortBy (fun _ -> randomizer.Next())
        |> Seq.map (fun (name, func) -> name, (TimeOperation 100000 func))

    let TimeOperationsAFewTimes funcsWithName =
        Seq.initInfinite (fun _ -> (TimeOperations funcsWithName))
        |> Seq.take 50
        |> Seq.concat
        |> Seq.groupBy fst
        |> Seq.map (fun (name, individualResults) -> name, (individualResults |> Seq.map snd |> Seq.average))

【讨论】:

FWIW,stopwatch.Elapsed.TotalSeconds 更准确。 IIRC,它的准确率提高了大约 100 倍。【参考方案19】:

F#、DataReaders 的 DataSetExtensions

System.Data.DataSetExtensions.dll 添加了将DataTable 视为IEnumerable&lt;DataRow&gt; 的功能,并通过支持System.Nullable 以优雅处理DBNull 的方式取消装箱单个单元格的值。例如,在 C# 中,我们可以获取包含空值的整数列的值,并使用非常简洁的语法指定 DBNull 应默认为零:

var total = myDataTable.AsEnumerable()
                       .Select(row => row.Field<int?>("MyColumn") ?? 0)
                       .Sum();

然而,DataSetExtensions 有两个方面缺乏。首先,它不支持IDataReader,其次,它不支持F# option 类型。以下代码两者兼而有之 - 它允许将 IDataReader 视为 seq&lt;IDataRecord&gt;,并且它可以从读取器或数据集中取消装箱值,并支持 F# 选项或 System.Nullable。结合选项合并运算符 in another answer,这允许在使用 DataReader 时使用如下代码:

let total =
    myReader.AsSeq
    |> Seq.map (fun row -> row.Field<int option>("MyColumn") |? 0)
    |> Seq.sum

也许一种更惯用的 F# 忽略数据库空值的方式是...

let total =
    myReader.AsSeq
    |> Seq.choose (fun row -> row.Field<int option>("MyColumn"))
    |> Seq.sum

此外,下面定义的扩展方法可用于 F# 和 C#/VB。

open System
open System.Data
open System.Reflection
open System.Runtime.CompilerServices
open Microsoft.FSharp.Collections

/// Ported from System.Data.DatasetExtensions.dll to add support for the Option type.
[<AbstractClass; Sealed>]
type private UnboxT<'a> private () =

    // This class generates a converter function based on the desired output type,
    // and then re-uses the converter function forever. Because the class itself is generic,
    // different output types get different cached converter functions.

    static let referenceField (value:obj) =
        if value = null || DBNull.Value.Equals(value) then
            Unchecked.defaultof<'a>
        else
            unbox value

    static let valueField (value:obj) =
        if value = null || DBNull.Value.Equals(value) then
            raise <| InvalidCastException("Null cannot be converted to " + typeof<'a>.Name)
        else
            unbox value

    static let makeConverter (target:Type) methodName =
        Delegate.CreateDelegate(typeof<Converter<obj,'a>>,
                                typeof<UnboxT<'a>>
                                    .GetMethod(methodName, BindingFlags.NonPublic ||| BindingFlags.Static)
                                    .MakeGenericMethod([| target.GetGenericArguments().[0] |]))
        |> unbox<Converter<obj,'a>>
        |> FSharpFunc.FromConverter

    static let unboxFn =
        let theType = typeof<'a>
        if theType.IsGenericType && not theType.IsGenericTypeDefinition then
            let genericType = theType.GetGenericTypeDefinition()
            if typedefof<Nullable<_>> = genericType then
                makeConverter theType "NullableField"
            elif typedefof<option<_>> = genericType then
                makeConverter theType "OptionField"
            else
                invalidOp "The only generic types supported are Option<T> and Nullable<T>."
        elif theType.IsValueType then
            valueField
        else
            referenceField

    static member private NullableField<'b when 'b : struct and 'b :> ValueType and 'b:(new:unit -> 'b)> (value:obj) =
        if value = null || DBNull.Value.Equals(value) then
            Nullable<_>()
        else
            Nullable<_>(unbox<'b> value)

    static member private OptionField<'b> (value:obj) =
        if value = null || DBNull.Value.Equals(value) then
            None
        else
            Some(unbox<'b> value)

    static member inline Unbox =
        unboxFn

/// F# data-related extension methods.
[<AutoOpen>]
module FsDataEx =

    type System.Data.IDataReader with

        /// Exposes a reader's current result set as seq<IDataRecord>.
        /// Reader is closed when sequence is fully enumerated.
        member this.AsSeq =
            seq  use reader = this
                  while reader.Read() do yield reader :> IDataRecord 

        /// Exposes all result sets in a reader as seq<seq<IDataRecord>>.
        /// Reader is closed when sequence is fully enumerated.
        member this.AsMultiSeq =
            let rowSeq (reader:IDataReader)  =
                seq  while reader.Read() do yield reader :> IDataRecord 
            seq 
                use reader = this
                yield rowSeq reader
                while reader.NextResult() do
                    yield rowSeq reader
            

        /// Populates a new DataSet with the contents of the reader. Closes the reader after completion.
        member this.ToDataSet () =
            use reader = this
            let dataSet = new DataSet(RemotingFormat=SerializationFormat.Binary, EnforceConstraints=false)
            dataSet.Load(reader, LoadOption.OverwriteChanges, [| "" |])
            dataSet

    type System.Data.IDataRecord with

        /// Gets a value from the record by name. 
        /// DBNull and null are returned as the default value for the type.
        /// Supports both nullable and option types.
        member this.Field<'a> (fieldName:string) =
            this.[fieldName] |> UnboxT<'a>.Unbox

        /// Gets a value from the record by column index. 
        /// DBNull and null are returned as the default value for the type.
        /// Supports both nullable and option types.
        member this.Field<'a> (ordinal:int) =
            this.GetValue(ordinal) |> UnboxT<'a>.Unbox

    type System.Data.DataRow with

        /// Identical to the Field method from DatasetExtensions, but supports the F# Option type.
        member this.Field2<'a> (columnName:string) =
            this.[columnName] |> UnboxT<'a>.Unbox

        /// Identical to the Field method from DatasetExtensions, but supports the F# Option type.
        member this.Field2<'a> (columnIndex:int) =
            this.[columnIndex] |> UnboxT<'a>.Unbox

        /// Identical to the Field method from DatasetExtensions, but supports the F# Option type.
        member this.Field2<'a> (column:DataColumn) =
            this.[column] |> UnboxT<'a>.Unbox

        /// Identical to the Field method from DatasetExtensions, but supports the F# Option type.
        member this.Field2<'a> (columnName:string, version:DataRowVersion) =
            this.[columnName, version] |> UnboxT<'a>.Unbox

        /// Identical to the Field method from DatasetExtensions, but supports the F# Option type.
        member this.Field2<'a> (columnIndex:int, version:DataRowVersion) =
            this.[columnIndex, version] |> UnboxT<'a>.Unbox

        /// Identical to the Field method from DatasetExtensions, but supports the F# Option type.
        member this.Field2<'a> (column:DataColumn, version:DataRowVersion) =
            this.[column, version] |> UnboxT<'a>.Unbox


/// C# data-related extension methods.
[<Extension; AbstractClass; Sealed>]
type CsDataEx private () =

    /// Populates a new DataSet with the contents of the reader. Closes the reader after completion.
    [<Extension>]    
    static member ToDataSet(this:IDataReader) =
        this.ToDataSet()

    /// Exposes a reader's current result set as IEnumerableIDataRecord.
    /// Reader is closed when sequence is fully enumerated.
    [<Extension>]
    static member AsEnumerable(this:IDataReader) =
        this.AsSeq

    /// Exposes all result sets in a reader as IEnumerableIEnumerableIDataRecord.
    /// Reader is closed when sequence is fully enumerated.
    [<Extension>]
    static member AsMultipleEnumerable(this:IDataReader) =
        this.AsMultiSeq

    /// Gets a value from the record by name. 
    /// DBNull and null are returned as the default value for the type.
    /// Supports both nullable and option types.
    [<Extension>]
    static member Field<'T> (this:IDataRecord, fieldName:string) =
        this.Field<'T>(fieldName)

    /// Gets a value from the record by column index. 
    /// DBNull and null are returned as the default value for the type.
    /// Supports both nullable and option types.
    [<Extension>]
    static member Field<'T> (this:IDataRecord, ordinal:int) =
        this.Field<'T>(ordinal)

【讨论】:

【参考方案20】:

在命令行应用程序中处理参数

//We assume that the actual meat is already defined in function 
//    DoStuff (string -> string -> string -> unit)
let defaultOutOption = "N"
let defaultUsageOption = "Y"

let usage =  
      "Scans a folder for and outputs results.\n" +
      "Usage:\n\t MyApplication.exe FolderPath [IncludeSubfolders (Y/N) : default=" + 
      defaultUsageOption + "] [OutputToFile (Y/N): default=" + defaultOutOption + "]"

let HandlArgs arr = 
    match arr with
        | [|d;u;o|] -> DoStuff d u o
        | [|d;u|] -> DoStuff d u defaultOutOption 
        | [|d|] -> DoStuff d defaultUsageOption defaultOutOption 
        | _ ->  
            printf "%s" usage
            Console.ReadLine() |> ignore

[<EntryPoint>]
let main (args : string array) = 
    args |> HandlArgs
    0

(我对受Robert Pickering 启发的这种技术有一个模糊的记忆,但现在找不到参考)

【讨论】:

PowerPack 已经带有一个漂亮的命令行参数解析器:laurent.le-brun.eu/site/index.php/2010/06/08/…【参考方案21】:

一个方便的缓存功能,可以将max (key,reader(key)) 保存在字典中,并使用SortedList 来跟踪MRU 密钥

let Cache (reader: 'key -> 'value) max = 
        let cache = new Dictionary<'key,LinkedListNode<'key * 'value>>()
        let keys = new LinkedList<'key * 'value>()

        fun (key : 'key) -> ( 
                              let found, value = cache.TryGetValue key
                              match found with
                              |true ->
                                  keys.Remove value
                                  keys.AddFirst value |> ignore
                                  (snd value.Value)

                              |false -> 
                                  let newValue = key,reader key
                                  let node = keys.AddFirst newValue
                                  cache.[key] <- node

                                  if (keys.Count > max) then
                                    let lastNode = keys.Last
                                    cache.Remove (fst lastNode.Value) |> ignore
                                    keys.RemoveLast() |> ignore

                                  (snd newValue))

【讨论】:

对不起,我的意思是 MRU(最近使用的)。将 reader 想象成一个访问远程数据库或 Web 服务甚至是非常繁重的计算的慢速查找函数。 是的,我可以看到缓存的用途,而不是“修剪”它。让我想知道我是否不应该在这里放一个 sn-p 用于记忆(如果我能找到一个!)【参考方案22】:

创建 XElements

没什么了不起,但我一直被 XNames 的隐式转换所困扰:

#r "System.Xml.Linq.dll"
open System.Xml.Linq

//No! ("type string not compatible with XName")
//let el = new XElement("MyElement", "text") 

//better
let xn s = XName.op_Implicit s
let el = new XElement(xn "MyElement", "text")

//or even
let xEl s o = new XElement(xn s, o)
let el = xEl "MyElement" "text"

【讨论】:

该转换是 .Net 的一部分,它重载了隐式 cast(String, XElement)。因此,任何支持强制转换重载的 .Net 语言都支持这一点。还是不错的功能。 @Dykam,恐怕比这复杂一点:codebetter.com/blogs/matthew.podwysocki/archive/2009/06/11/… 啊,解释了这个功能。但是在扫描时,我无法找出为什么 F# 不支持强制转换运算符。【参考方案23】:

成对和成对

我总是希望 Seq.pairwise 给我 [(1,2);(3;4)] 而不是 [(1,2);(2,3);(3,4)]。鉴于 List 中都不存在,并且我需要两者,这是供将来参考的代码。我think they're tail recursive。

//converts to 'windowed tuples' ([1;2;3;4;5] -> [(1,2);(2,3);(3,4);(4,5)])
let pairwise lst = 
    let rec loop prev rem acc = 
       match rem with
       | hd::tl -> loop hd tl ((prev,hd)::acc)
       | _ -> List.rev acc
    loop (List.head lst) (List.tail lst) []

//converts to 'paged tuples' ([1;2;3;4;5;6] -> [(1,2);(3,4);(5,6)])    
let pairs lst = 
    let rec loop rem acc = 
       match rem with
       | l::r::tl -> loop tl ((l,r)::acc)
       | l::[] -> failwith "odd-numbered list" 
       | _ -> List.rev acc
    loop lst []

【讨论】:

【参考方案24】:

简单的 CSV 阅读器(即,不会处理任何讨厌的事情)

(在此处使用其他答案中的 filereadlines 和 List.transpose)

///Given a file path, returns a List of row lists
let ReadCSV = 
        filereadlines
            >> Array.map ( fun line -> line.Split([|',';';'|]) |> List.ofArray )
            >> Array.toList

///takes list of col ids and list of rows, 
///   returns array of columns (in requested order)
let GetColumns cols rows = 
    //Create filter
    let pick cols (row:list<'a>) = List.map (fun i -> row.[i]) cols

    rows 
        |> transpose //change list of rows to list of columns
        |> pick cols      //pick out the columns we want
        |> Array.ofList  //an array output is easier to index for user

例子

"C:\MySampleCSV"
   |> ReadCSV
   |> List.tail //skip header line
   |> GetColumns [0;3;1]  //reorder columns as well, if needs be.

【讨论】:

【参考方案25】:

日期范围

fromDatetoDate 之间的简单但有用的日期列表

let getDateRange  fromDate toDate  =

    let rec dates (fromDate:System.DateTime) (toDate:System.DateTime) = 
        seq 
            if fromDate <= toDate then 
                yield fromDate
                yield! dates (fromDate.AddDays(1.0)) toDate
            

    dates fromDate toDate
    |> List.ofSeq

【讨论】:

【参考方案26】:

将代码切换到 sql

比这个列表中的大多数都更琐碎,但仍然很方便:

我总是在开发过程中将 sql 放入和取出代码以将其移动到 sql 环境中。示例:

let sql = "select a,b,c "
    + "from table "
    + "where a = 1"

需要被“剥离”到:

select a,b,c
from table
where a = 1

保持格式。删除 sql 编辑器的代码符号,然后在我解决了 sql 后手动将它们放回原处是一件很痛苦的事情。这两个函数在代码和剥离的 sql 之间来回切换:

// reads the file with the code quoted sql, strips code symbols, dumps to FSI
let stripForSql fileName = 
    File.ReadAllText(fileName)
    |> (fun s -> Regex.Replace(s, "\+(\s*)\"", "")) 
    |> (fun s -> s.Replace("\"", ""))
    |> (fun s -> Regex.Replace(s, ";$", "")) // end of line semicolons
    |> (fun s -> Regex.Replace(s, "//.+", "")) // get rid of any comments
    |> (fun s -> printfn "%s" s)

那么当你准备好把它放回你的代码源文件时:

let prepFromSql fileName = 
    File.ReadAllText(fileName)
    |> (fun s -> Regex.Replace(s, @"\r\n", " \"\r\n+\"")) // matches newline 
    |> (fun s -> Regex.Replace(s, @"\A", " \"")) 
    |> (fun s -> Regex.Replace(s, @"\z", " \"")) 
    |> (fun s -> printfn "%s" s)

喜欢摆脱输入文件,但甚至无法开始思考如何实现这一目标。有人吗?

编辑:

我想出了如何通过添加 Windows 窗体对话框输入/输出来消除对这些功能的文件要求。代码太多要显示,但是对于那些想做这种事情的人,我就是这样解决的。

【讨论】:

没有编译器来回答你的最后一个问题,但我会用这些来让你的管道更漂亮:let replace f r (s:string) = s.Replace(f,r)let regreplace p r s = Regex.Replace(s, p, r)(未经测试)【参考方案27】:

帕斯卡三角(嘿,有人可能会觉得它有用)

所以我们想创建一个这样的东西:

       1
      1 1
     1 2 1
    1 3 3 1
   1 4 6 4 1

很简单:

let rec next = function
    | [] -> []
    | x::y::xs -> (x + y)::next (y::xs)
    | x::xs -> x::next xs

let pascal n =
    seq  1 .. n 
    |> List.scan (fun acc _ -> next (0::acc) ) [1]

next 函数返回一个新列表,其中每个 item[i] = item[i] + item[i + 1]。

这是 fsi 中的输出:

> pascal 10 |> Seq.iter (printfn "%A");;
[1]
[1; 1]
[1; 2; 1]
[1; 3; 3; 1]
[1; 4; 6; 4; 1]
[1; 5; 10; 10; 5; 1]
[1; 6; 15; 20; 15; 6; 1]
[1; 7; 21; 35; 35; 21; 7; 1]
[1; 8; 28; 56; 70; 56; 28; 8; 1]
[1; 9; 36; 84; 126; 126; 84; 36; 9; 1]
[1; 10; 45; 120; 210; 252; 210; 120; 45; 10; 1]

对于喜欢冒险的人,这里有一个尾递归版本:

let rec next2 cont = function
    | [] -> cont []
    | x::y::xs -> next2 (fun l -> cont <| (x + y)::l ) <| y::xs
    | x::xs -> next2 (fun l -> cont <| x::l ) <| xs

let pascal2 n =
    set  1 .. n 
    |> Seq.scan (fun acc _ -> next2 id <| 0::acc)) [1]

【讨论】:

另见:***.com/questions/1242073/… 现在你只需要漂亮地打印出来 :) ***.com/questions/1733311/pretty-print-a-tree【参考方案28】:

扁平化列表

如果你有这样的事情:

let listList = [[1;2;3;];[4;5;6]] 

并希望将其“展平”为单个列表,因此结果如下:

[1;2;3;4;5;6]

这样就可以了:

let flatten (l: 'a list list) =
    seq 
            yield List.head (List.head l) 
            for a in l do yield! (Seq.skip 1 a) 
         

    |> List.ofSeq

【讨论】:

非常抱歉,但我认为它已经存在:它是 List.concat。 (这一直发生在我身上——编写一个函数然后发现它已经存在了!)。看看是否有一个函数可以“递归”执行此操作(即对于[[[1;2;3;];[4;5;6]];[[1;2;3;];[4;5;6]]] 哇!伙计——我真的在寻找一种方法来做这个 b4 我自己滚动。 List.concat 绝对是这样做的方法,在我发现它之前我使用的是List.collect id【参考方案29】:

float 的列表推导

[23.0 .. 1.0 .. 40.0] 被标记为已弃用,有几个版本支持。

但显然,这是可行的:

let dl = 9.5 / 11.
let min = 21.5 + dl
let max = 40.5 - dl

let a = [ for z in min .. dl .. max -> z ]
let b = a.Length

(顺便说一句,里面有一个浮点问题。发现于 fssnip - F# sn-ps 的另一个地方)

【讨论】:

这个不稳定,见***.com/questions/377078/…【参考方案30】:

平行图

let pmap f s =
    seq  for a in s -> async  return f s  
    |> Async.Parallel
    |> Async.Run

【讨论】:

对于 CPU 密集型工作,异步工作流会产生高开销和较差的负载平衡,因此对于并行性来说这是一个糟糕的解决方案。现在使用内置的Array.Parallel.map 好多了。

以上是关于方便的 F# 片段 [关闭]的主要内容,如果未能解决你的问题,请参考以下文章

winform 新增时新打开窗口,当关闭时刷新原有列表页面,不要用ShowDialog,要比较普通方便的方法

android:当用户触摸片段外部时,我如何关闭片段?

在导航抽屉关闭之前加载片段

带有片段的操作栏的行为? [关闭]

安卓。通过从片段中的按钮调用片段中的方法来关闭片段?

如何在片段中使用按钮[关闭]