Фразы Handy F #

Уже есть два questions об F #/функциональных фрагментах.

Однако я ищу здесь полезные фрагменты, маленькие "вспомогательные" функции, которые можно использовать повторно. Или неясные, но изящные шаблоны, которые вы никогда не сможете запомнить.

Что-то вроде:

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} 

Я хотел бы сделать это своего рода справочной страницей. Как таковой не будет правильного ответа, но, надеюсь, много хороших.

EDIT Tomas Petricek создал сайт специально для фрагментов F # http://fssnip.net/.

Ответ 1

Соответствие регулярному выражению стиля Perl

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

Он позволяет вам сопоставлять текст с использованием нотации let test = "monkey" =~ "monk.+".

Ответ 2

Оператор Infix

Я получил это от 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 # не имеет истинного оператора "infix", то же самое можно сделать почти так же элегантно через конвейер и "backpipeline" (кто знал такую ​​вещь??)

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

Ответ 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 # видит обратную косую черту, за которой следует возврат каретки внутри строкового литерала, он удалит все из обратного слэша в первый непространственный символ на следующей строке. Это позволяет вам иметь многострочные строковые литералы, которые выстраиваются в линию, не используя кучу конкатенации строк.

Ответ 4

Общее мемонирование, любезно предоставлено сам человек

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

Ответ 5

Для высокопроизводительных материалов, где вам нужно проверить null

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

примерно в 20 раз быстрее, чем

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

Ответ 6

Простые чтения-записи в текстовые файлы

Это тривиально, но сделать доступ к файлам доступным:

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)

Обновить Небольшое улучшение, если вы хотите читать "заблокированные" файлы (например, файлы csv, которые уже открыты в Excel...):

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  

Ответ 7

Активные шаблоны, также называемые "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

"Унифицировать" функцию, которая не обрабатывает единицы Использование функции 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>

OLD version:

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

Престижность kvb

Ответ 9

Возможно, монада

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 для непосвященных.

Ответ 10

Операторы Option-coalescing

Мне нужна версия функции defaultArg, которая имела синтаксис ближе к оператору с нулевым коалесцированием С#, ??. Это позволяет мне получить значение из параметра, предоставляя значение по умолчанию, используя очень сжатый синтаксис.

/// 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()

Ответ 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)

///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

Если бы я был умным, я бы попытался распараллелить его с помощью async...

Ответ 13

F # Карта ↔ Словарь С#

(Я знаю, я знаю, System.Collections.Generic.Dictionary на самом деле не является словарем С#)

С# в F #

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

(От Брайана, здесь, с улучшением, предложенным Mauricio в комментарии ниже. (|KeyValue|) - активный шаблон для сопоставления KeyValuePair - от FSharp.Core - эквивалентно (fun kvp -> kvp.Key, kvp.Value))

Интересная альтернатива

Чтобы получить всю неизменную доброту, но с помощью скорости поиска O (1) словаря, вы можете использовать оператор dict, который возвращает неизменяемый IDictionary (см. this вопрос).

В настоящее время я не вижу способа прямого преобразования словаря с помощью этого метода, кроме

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

F # до С#

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; } ...

Ответ 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]

Ответ 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)

Ответ 16

Взвешенная сумма массивов

Вычисление взвешенной [n-array] суммы [k-массива n-массивов] чисел на основе [k-массива] весов

(Скопировано из этого вопроса и kvb ответ)

Учитывая эти массивы

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((*)), которая применяется (для каждого веса) к каждому массиву в обр.

Вторая строка: Array.reduce похожа на fold, за исключением того, что она начинается со второго значения и использует первое как начальное "состояние". В этом случае каждое значение является "линией" нашего массива массивов. Таким образом, применение Array.map2 (+) в первых двух строках означает, что мы суммируем первые два массива, что оставляет нас с новым массивом, который мы затем (Array.reduce) снова суммируем на следующем (в последнем случае) массив.

Результат:

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

Ответ 17

Тестирование производительности

(Найдено здесь и обновлен для последней версии 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))

Ответ 18

ОК, это не имеет ничего общего с фрагментами, но я все время забываю об этом:

Если вы находитесь в интерактивном окне, нажмите F7, чтобы вернуться в окно кода (без отмены кода, который вы только что запустили...)

Переход из окна кода в окно F # (а также для открытия окна F #) - это Ctrl Alt F

(если CodeRush не украл ваши привязки...)

Ответ 19

DataSetExtensions для F #, DataReaders

System.Data.DataSetExtensions.dll добавляет возможность обрабатывать DataTable как IEnumerable<DataRow>, а также распаковывать значения отдельных ячеек таким образом, чтобы изящно обрабатывать DBNull, поддерживая System.Nullable. Например, в С# мы можем получить значение целочисленного столбца, содержащего нули, и указать, что DBNull должен иметь значение по умолчанию с нулевым синтаксисом:

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

Есть две области, где отсутствуют DataSetExtensions. Во-первых, он не поддерживает IDataReader, а во-вторых, он не поддерживает тип F # option. Следующий код делает это: он позволяет обрабатывать IDataReader как seq<IDataRecord>, и он может удалять значения из считывателя или набора данных с поддержкой параметров F # или System.Nullable. В сочетании с оператором коалесценции в другом ответе это позволяет использовать код, например, при работе с 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 #, так и из С#/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 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 current result set as IEnumerable{IDataRecord}.
    /// Reader is closed when sequence is fully enumerated.
    [<Extension>]
    static member AsEnumerable(this:IDataReader) =
        this.AsSeq

    /// Exposes all result sets in a reader as IEnumerable{IEnumerable{IDataRecord}}.
    /// 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

Удобная функция кеширования, которая поддерживает 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))

Ответ 21

Обработка аргументов в приложении командной строки:

//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, но теперь не найти ссылку)

Ответ 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"

Ответ 23

Пара и пары

Я всегда ожидаю, что Seq.pairwise даст мне [(1,2); (3; 4)], а не [(1,2); (2,3); (3,4)]. Учитывая, что в списке нет ни одного, и что мне нужны оба, вот код для дальнейшего использования. Я думаю, что они рекурсивные хвосты.

//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

Треугольник Паскаля (эй, кому-то это может показаться полезным)

Итак, мы хотим создать что-то вроде этого:

       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 возвращает новый список, в котором каждый элемент [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]

Ответ 26

Диапазон дат

простой, но полезный список дат между fromDate и toDate

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

Ответ 27

переключить код на 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)

Я бы люблю, чтобы избавиться от входного файла, но даже не могу понять, как это сделать. кто-нибудь?

редактировать:

Я понял, как устранить требование файла для этих функций, добавив диалог ввода/вывода диалогового окна. Слишком много кода для показа, но для тех, кто хотел бы сделать такое, как я его решил.

Ответ 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

Ответ 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 #)

Ответ 30

Параллельная карта

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