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

时间:2016-02-22 17:38:31

标签: f# functional-programming f#-interactive f#-3.0

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

我有一个格式为

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

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

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

我找到了无数种方法来连接列表元素,到目前为止我的最佳猜测(展开,或类似的东西)都没有结果。任何帮助或正确方向的一点将非常感激。谢谢!

6 个答案:

答案 0 :(得分: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

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易于解决的更灵活的格式规则。

答案 1 :(得分: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#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位数字,这将把它们分成列表中的单个元素。如果您不想要,则必须使用正则表达式或string.split或其他内容。

答案 2 :(得分: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

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

答案 3 :(得分: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)

结果与以前相同。

答案 4 :(得分: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"]]

答案 5 :(得分: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()函数并将结果数组转换为列表。希望这有帮助