Как манипулировать элементами списка в 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()
функция и поворачивает результирующий массив в список. Надеюсь, это поможет