如何在 F# 中操作列表元素

How to manipulate list elements in F#

我目前正在使用 F# 完成一个项目。我对函数式编程很陌生,虽然我熟悉列表项不可变的想法,但我仍然遇到一些问题:

我有一个格式为

的字符串列表
["(states, (1,2,3,4,5))"; "(alpha, (1,2))"; "(final, (1))"]

我想做的是将每个列表元素变成它自己的列表,而不使用初始逗号分隔的字符串。输出应如下所示:

["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 的元组,无论如何上面的代码所做的是删除第一个元组的 item 然后使用剩余的 可变大小元组 (括号内的数字),然后调用 .Net string.Split() 函数并将结果数组转换为列表。希望这有帮助

正如@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

我喜欢这个解决方案的地方在于它不使用幻数,但它的核心只是一个正则表达式。同样将每个匹配项解析为整数是非常安全的,因为我们只匹配数字。

与 Luiso 的回答类似,但应避免出现异常。请注意,我在 '('')' 上拆分,这样我就可以隔离元组。然后我尝试仅在 ',' 上拆分元组以获得最终结果之前获取元组。我使用模式匹配来避免异常。

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"]]

您也可以像这样使用 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# 交互式中对其进行测试:

> 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 位的数字,这会将它们拆分为列表中的各个元素。如果你不想要,你将不得不使用正则表达式或 string.split 或其他东西。

您可以使用 .NET 中的 内置字符串操作 API 实现此目的。你不必让它特别花哨,但它有助于在 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)" 等,具体取决于作为第一个参数。

为了解析像 "(1,2,3,4,5)""(1,2)" 这样的数字,您可以将 betweenParentheses 与 FParsec 的内置 sepBypint32 结合使用:

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

pint32 是整数解析器,sepBy 是读取值列表的解析器,值列表由字符串分隔 - 在本例中为 ",".

为了解析整个 'group' 值,例如 "(states, (1,2,3,4,5))""(alpha, (1,2))",您可以再次使用 betweenParenthesespnumbers

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

由于这个函数returns一个选项,可以使用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.