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

9
我是您的助手,以下为您翻译的内容:

我目前正在使用F#完成一个项目。虽然我对函数式编程的概念很熟悉,但是我仍然有一些问题:

我有一个字符串列表,格式如下:

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

我想做的是将每个列表元素转换为自己的列表,而不是初始逗号分隔字符串。输出应该看起来像这样:
["1"; "2"; "3"; "4"; "5"]
["1"; "2"]
["1"]

我已经尝试过多种方法来连接列表元素,但是我的最佳猜测(展开或类似的方法)都没有成功。如果能提供帮助或指引方向,将不胜感激。谢谢!

3
首先,将问题分解为单个列表元素。从提取该格式字符串中的整数列表开始(我可以建议使用正则表达式)。从那时起,只需将其映射到列表中的每个字符串上(List.map),这只是一个问题。 - Jwosty
6个回答

7

为了好玩,这里提供使用FParsec解析字符串的概要,它是一个组合器库。

首先,你需要导入一些模块:

open FParsec.Primitives
open FParsec.CharParsers

然后,您可以定义一个解析器,该解析器将匹配括号内的所有字符串:
let betweenParentheses p s = between (pstring "(") (pstring ")") p s

这将匹配任何括号中的字符串,例如"(42)""(foo)""(1,2,3,4,5)"等,具体取决于作为第一个参数传递的特定解析器p
为了解析像"(1,2,3,4,5)""(1,2)"这样的数字,您可以将betweenParentheses与FParsec的内置sepBypint32组合使用。
let pnumbers s = betweenParentheses (sepBy pint32 (pstring ",")) s

pint32是一个整数解析器,sepBy是一个解析器,读取由字符串-在这种情况下为","分隔的值列表。

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

let pgroup s =
    betweenParentheses
        (manyTill anyChar (pstring ",") >>. spaces >>. pnumbers) s
< p > 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更灵活的格式化规则。

5
您可以直接使用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个数字,这将把它们分成列表中的单个元素。如果你不想这样做,你需要使用正则表达式、字符串拆分或其他方法。

4

正如@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 进行传递即可。

我喜欢这个解决方案的原因是它没有使用数字魔术,而其核心只是一个普通的正则表达式。还有,将每个匹配项解析为整数非常安全,因为我们只匹配数字。


4
你可以使用.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)

结果与以前相同。

1
不是很重要,但有人认为应该用 Seq.item 1 替换 Seq.tail >> Seq.head - 这样更加明显(但 [还是很容易] 会出现超出索引范围的情况)? - Ruben Bartelink
1
作为一个完全的旁白,可以将trySecond写成let trySecond = function | xs when xs |> Seq.length > 1 -> xs |> Seq.item 1 |> Some | _ -> None 或者 let trySecond = function | xs when Seq.length xs > 1 -> Seq.item 1 xs |> Some | _ -> None(尽管在长集合的情况下,Seq.length的性能实际上可能比Seq.toList差,这取决于后者如何实现)。 - Ruben Bartelink
1
x |> List.map fn |> List.choose id 不就等同于 x |> List.choose fn 吗? - Johannes Egger
@JohannesEgger 我想这很有可能 :) - Mark Seemann
1
或者是这个函数的简化版 let rec tryItem index = if index = 0 then Seq.tryHead else tryItem (index-1)。等等...它已经内置了:let trySecond = Seq.tryItem 1 :) - Ruben Bartelink

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

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接