Как манипулировать элементами списка в F#

в настоящее время я работаю над проектом, используя F#. Я совершенно новичок в функциональном программировании, и хотя я знаком с идеей неизменности элементов списка, у меня все еще есть небольшая проблема:

у меня есть список строк в формате

["(states, (1,2,3,4,5))"; "(alpha, (1,2))"; "(final, (1))"]

что я хотел бы сделать, это превратить каждый элемент списка в свой собственный список без начальной строки, разделенной запятыми. Вывод должен выглядеть примерно так:

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

Я нашел мириады способов объединения элементов списка и мои лучшие догадки до сих пор (разворачивающиеся или что-то в этом роде) были бесплодны. Любая помощь или точку в правильном направлении будет высоко ценится. Спасибо!

6 ответов


вы можете достичь этого с встроенный API манипуляции строками in .Сеть. Вам не нужно делать это особенно причудливым, но это помогает обеспечить некоторые тонкие адаптеры Карри над string API:

open System

let removeWhitespace (x : string) = x.Replace(" ", "")

let splitOn (separator : string) (x : string) =
    x.Split([| separator |], StringSplitOptions.RemoveEmptyEntries)

let trim c (x : string) = x.Trim [| c |]

единственный немного сложный шаг, как только вы использовали splitOn разделить "(states, (1,2,3,4,5))" на [|"(states"; "1,2,3,4,5))"|]. Теперь у вас есть массив с двумя элементами, и вы хотите второй элемент. Вы можете сделать это, взяв Seq.tail этого массива, выбрасывая первые стихия, а потом взятие Seq.head в результирующей последовательности, давая вам первый элемент оставшейся последовательности.

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

let result =
    ["(states, (1,2,3,4,5))"; "(alpha, (1,2))"; "(final, (1))"]
    |> List.map (
        removeWhitespace
        >> splitOn ",("
        >> Seq.tail
        >> Seq.head
        >> trim ')'
        >> splitOn ","
        >> Array.toList)

результат:

val result : string list list = [["1"; "2"; "3"; "4"; "5"]; ["1"; "2"]; ["1"]]

самая опасная часть -Seq.tail >> Seq.head комбинации. Он может завершиться ошибкой, если входной список содержит менее двух элементов. Более безопасной альтернативой было бы использовать что-то вроде следующего trySecond помощник функция:

let trySecond xs =
    match xs |> Seq.truncate 2 |> Seq.toList with
    | [_; second] -> Some second
    | _ -> None

используя эту функцию, вы можете переписать функцию извлечения данных, чтобы быть немного более надежным:

let result' =
    ["(states, (1,2,3,4,5))"; "(alpha, (1,2))"; "(final, (1))"]
    |> List.map (removeWhitespace >> splitOn ",(" >> trySecond)
    |> List.choose id
    |> List.map (trim ')' >> splitOn "," >> Array.toList)

результат тот же, что и раньше.


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

во-первых, вы импортируете некоторые модули:

open FParsec.Primitives
open FParsec.CharParsers

затем вы можете определить синтаксический анализатор, который будет соответствовать всем строкам, заключенным в круглые скобки:

let betweenParentheses p s = between (pstring "(") (pstring ")") p s

это будет соответствовать любой строке, заключенной в скобки, например "(42)", "(foo)", "(1,2,3,4,5)", etc. в зависимости от конкретного парсера p прошло как первое аргумент.

для того, чтобы разобрать цифры, как "(1,2,3,4,5)" или "(1,2)", вы можете комбинировать betweenParentheses со встроенным FParsec sepBy и pint32:

let pnumbers s = betweenParentheses (sepBy pint32 (pstring ",")) s

pint32 - парсер целых чисел, и sepBy - парсер, считывающий список значений, разделенных строкой-в данном случае ",".

для анализа всей "группы" значений, таких как "(states, (1,2,3,4,5))" или "(alpha, (1,2))", вы можете снова использовать betweenParentheses и pnumbers:

let pgroup s =
    betweenParentheses
        (manyTill anyChar (pstring ",") >>. spaces >>. pnumbers) s

в manyTill комбинация анализирует любую char значение, пока он не встречает ,. Далее pgroup парсер ожидает любое количество пробелов, а затем формат, определенный pnumbers.

наконец, вы можете определить функцию, которая выполняет pgroup парсер на строку:

// string -> int32 list option
let parseGroup s =
    match run pgroup s with
    | Success (result, _, _) -> Some result
    | Failure _              -> None

так как эта функция возвращает параметр, вы можете использовать List.choose чтобы отобразить строки, которые могут быть проанализированы:

> ["(states, (1,2,3,4,5))"; "(alpha, (1,2))"; "(final, (1))"]
  |> List.choose parseGroup;;
val it : int32 list list = [[1; 2; 3; 4; 5]; [1; 2]; [1]]

использование FParsec, скорее всего, излишне, если у вас нет более гибкие правила форматирования, чем то, что можно легко решить с помощью стандарта .NET string API-интерфейс.


вы также можете просто использовать Char.IsDigit (по крайней мере, на основе ваших данных образца) так:

open System

// Signature is string -> string list
let getDigits (input : string) =
    input.ToCharArray()
    |> Array.filter Char.IsDigit
    |> Array.map (fun c -> c.ToString())
    |> List.ofArray

// signature is string list -> string list list
let convertToDigits input =
    input
    |> List.map getDigits

и тестирование его в F# interactive:

> let sampleData = ["(states, (1,2,3,4,5))"; "(alpha, (1,2))"; "(final, (1))"];;

val sampleData : string list =
  ["(states, (1,2,3,4,5))"; "(alpha, (1,2))"; "(final, (1))"]

> let test = convertToDigits sampleData;;

val test : string list list = [["1"; "2"; "3"; "4"; "5"]; ["1"; "2"]; ["1"]]

Примечание: Если у вас есть более 1 цифр, это разделит их на отдельные элементы в списке. Если вы этого не хотите, вам придется использовать regex или string.раскол или что-то еще.


Как предложил @JWosty, начните с одного элемента списка и сопоставьте его с помощью регулярных выражений.

let text = "(states, (1,2,3,4,5))"
// Match all numbers into group "number"
let pattern = @"^\(\w+,\s*\((?:(?<number>\d+),)*(?<number>\d+)\)$"
let numberMatch = System.Text.RegularExpressions.Regex.Match(text, pattern)
let values =
    numberMatch.Groups.["number"].Captures // get all matches from the group
    |> Seq.cast<Capture> // cast each item because regex captures are non-generic (i.e. IEnumerable instead of IEnumerable<'a>)
    |> Seq.map (fun m -> m.Value) // get the matched (string) value for each capture
    |> Seq.map int // parse as int
    |> Seq.toList // listify

делать это для списка входных текстов-это просто вопрос передачи этой логики в List.map.

что мне нравится в этом решении, так это то, что оно не использует магические числа, но ядро его-просто регулярное выражение. Также разбор каждого совпадения как целого довольно безопасен, потому что мы сопоставляем только цифры.


подобно ответу Луисо, но следует избегать исключений. Обратите внимание, что я разделился на '(' и ')' таким образом, я могу изолировать кортеж. Затем я пытаюсь получить Кортеж только перед разделением его на ',' чтобы получить конечный результат. Я использую сопоставление шаблонов, чтобы избежать исключений.

open System 

let values = ["(states, (1,2,3,4,5))"; "(alpha, (1,2))"; "(final, (1))"]


let new_list = values |> List.map(fun i -> i.Split([|'(';')'|], StringSplitOptions.RemoveEmptyEntries))
                          |> List.map(fun i -> i|> Array.tryItem(1))
                          |> List.map(function x -> match x with
                                                    | Some i -> i.Split(',') |> Array.toList
                                                    | None -> [])

printfn "%A" new_list

дает вам:

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

этот фрагмент должен сделать о вас спросить:

let values = ["(states, (1,2,3,4,5))"; "(alpha, (1,2))"; "(final, (1))"]

let mapper (value:string) = 
    let index = value.IndexOf('(', 2) + 1;
    value.Substring(index, value.Length - index - 2).Split(',') |> Array.toList 

values |> List.map mapper

выход:

val it : string list list = [["1"; "2"; "3"; "4"; "5"]; ["1"; "2"]; ["1"]]

как я вижу, каждый элемент в исходном списке является кортежем string и кортеж int переменного размера, в любом случае, что делает код выше, это удаление первого элемент кортежа, а затем использовать оставшиеся кортеж переменного размера (номера внутри parens), затем вызовите .Net string.Split() функция и поворачивает результирующий массив в список. Надеюсь, это поможет