如何在 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 的内置 sepBy
和 pint32
结合使用:
let pnumbers s = betweenParentheses (sepBy pint32 (pstring ",")) s
pint32
是整数解析器,sepBy
是读取值列表的解析器,值列表由字符串分隔 - 在本例中为 ","
.
为了解析整个 'group' 值,例如 "(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
由于这个函数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.
我目前正在使用 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 的内置 sepBy
和 pint32
结合使用:
let pnumbers s = betweenParentheses (sepBy pint32 (pstring ",")) s
pint32
是整数解析器,sepBy
是读取值列表的解析器,值列表由字符串分隔 - 在本例中为 ","
.
为了解析整个 'group' 值,例如 "(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
由于这个函数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.