【问题标题】:F# How to parse and evaluate calculator expressions with string inputF#如何使用字符串输入解析和评估计算器表达式
【发布时间】:2016-11-03 22:59:56
【问题描述】:

我必须在 F# 中创建一个计算器,并且我被困在一个任务上。

我需要将总和作为字符串传递到控制台,例如"4 + 5" 并解析计算。

有什么想法吗?

任何帮助将不胜感激

open System

let rec calculator() =
    printf "Choose a sum type \n1: Addition\n2: Subtraction\n3: Mulitiplication\n4: Division\n\n\n"

    let input = Console.ReadLine();

    printf "now type in 2 numbers\n"

    let num1 = Console.ReadLine();
    let num2 = Console.ReadLine();

    let a : int = int32 num1
    let b : int = int32 num2

    let addition x y = x + y
    let subtraction x y = x - y
    let multiply x y = x * y
    let divide x y = x / y 

    match input with

    | "1" -> printfn("The result is: %d")(addition a b)
    | "2" -> printfn("The result is: %d")(subtraction a b)
    | "3" -> printfn("The result is: %d")(multiply a b)
    | "4" -> printfn("The result is: %d")(divide a b)

    ignore(Console.ReadKey())
    ignore(calculator())

calculator()

【问题讨论】:

标签: string parsing f# calculator f#-interactive


【解决方案1】:

我需要将总和作为字符串传递到控制台,例如"4 + 5" 并解析计算。

如果您确定您的字符串是由'+' 和可能的空格分隔的数字序列,您可以执行以下操作:

"4 + 5".Split '+' |> Seq.sumBy (int)

它有什么作用? .Split '+' 用字符 + 分隔字符串并创建字符串序列。在这个例子中,序列看起来像[|"4 "; " 5"|]。函数Seq.sumBy 将给定函数应用于序列的每个元素并对结果求和。我们使用函数(int)将字符串转换为数字。

请注意,如果字符串包含+ 以外的字符、空格和数字,或者如果没有数字的字符串由+ 分隔(例如+ 7 + 87 ++ 8),则此解决方案将失败。

您可能想捕捉System.FormatException。你最终会得到类似的东西

let sumString (input:string) : int = 
    try
        input.Split '+' |> Seq.sumBy (int)
    with
    | :? System.FormatException ->
        print "This does not look like a sum. Let's just assume the result is zero."
        0

这只会为任何无效公式输出0。避免异常的另一个选择是丢弃所有不需要的字符和空字符串:

 let sumString (input:System.String) : int = 
    (input |> String.filter (fun c -> ['0'; '1'; '2'; '3'; '4'; '5'; '6'; '7'; '8'; '9'; '+'] |> List.contains c)).Split '+'
    |> Seq.filter (((<) 0) << String.length)
    |> Seq.sumBy (int)

这段代码有什么作用? String.filter 询问我们的匿名函数是否应考虑每个字符。我们的匿名函数检查字符是否在允许的字符列表中。结果是一个只包含数字和+ 的新字符串。我们用+分割这个字符串。

在将字符串列表传递给Seq.sumBy (int) 之前,我们过滤空字符串。这是通过Seq.filter 和函数组合完成的:如果第一个参数小于第二个参数,(&lt;) 返回true。我们使用柯里化来获得(&lt;) 0,它检查给定的整数是否大于0。我们用String.length 组成这个函数,它将一个字符串映射到一个整数,告诉它它的长度。

在让Seq.filter 使用此函数后,我们将结果列表传递给Seq.sumBy (int),如上。

然而,这可能会导致除总和之外的任何其他结果非常令人惊讶。 "4 * 5 + 7" 将产生 52

【讨论】:

    【解决方案2】:

    以稳健的方式解析像4 + 5 这样的表达式通常需要依赖像FsLex/FsYaccFParsec 这样的好工具。

    经验丰富的开发人员喜欢从头开始做事(并不总是一件好事),他们可能会实现称为 Recursive-Decent-Parser 的东西。

    其他人发现他们所有的解析都需要RegEx 来回答。但是,RegEx 的功能有限。例如,您如何定义正确解析以下表达式的RegEx3 + 3*(x+3)/2。此外,RegEx 代码往往晦涩难懂且难以解码,请考虑使用此常见的 sn-p 进行电子邮件验证:

    ^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$
    

    除了复杂之外,它也是不完整的。更完整的可以在这里找到:Using a regular expression to validate an email address

    此外,RegEx 往往不会产生任何错误消息来告诉用户表达式解析失败的原因。

    许多人使用的另一种常见(不完整且低效)的解析方法是使用String.Split 来拆分运算符上的字符串。

    例如:

    let r = 
      "1+2+3".Split '+'             // Produces an array: [|"1", "2", "3"|]
      |> Seq.map int                // Maps to seq of ints
      |> Seq.reduce (+)             // Reduces the seq using (+)
    

    根据合乎逻辑的结论,您最终可以得到一个看起来像这样的解析器

    // Very inefficient, can't handle sub expressions, no error reporting...
    //  Please use this as an illustration only, not production code
    let stupidParse =
      let split (c: char) (s: string) = s.Split c
      let trim (s: string) = s.Trim ()
      let op c r f  = split c >> Seq.map (trim >> f) >> Seq.reduce r
      int |> op '/' (/) |> op '*' (*) |> op '-' (-) |> op '+' (+)
    
    [<EntryPoint>]
    let main argv = 
      let examples = [| "1"; "1-3"; "1*3"; "1 + 2*3 - 2" |]
      for example in examples do
        printfn "%s -> %d" example <| stupidParse example
      0
    

    但正如评论中所说,我永远不希望这样的解析器输入生产代码。使用适当的工具,例如 FsLex/FsYaccFParsec

    【讨论】:

      猜你喜欢
      • 2013-10-03
      • 1970-01-01
      • 2010-09-27
      • 1970-01-01
      • 1970-01-01
      • 2016-04-24
      • 1970-01-01
      • 2023-04-06
      • 1970-01-01
      相关资源
      最近更新 更多