函数应该如何处理无效的数据集?

时间:2016-05-12 18:03:07

标签: f# functional-programming

函数应该如何处理无效的数据集?

我一直认为异常是OOP中的对象。 在FP中,我该如何处理无效数据?

我研究了面向铁路的编程。但是,我认为该技术适用于系统的边界接口,而不是其核心。

以下函数接受播放列表(按顺序),并根据指定基数的有序播放集分配基础。最终状态:

let assignBases (plays:Play list) =

    let initializedBases = { First=None; Second=None; Third=None }

    match plays with
    | [] -> initializedBases
    | _  -> let move bases play = 
                match (bases, play.Hit)  with
                | { First= None; Second=None; Third=None }, Single -> { bases with First=  Some play.Player }
                | { First= None; Second=None; Third=None }, Double -> { bases with Second= Some play.Player }
                | { First= None; Second=None; Third=None }, Triple -> { bases with Third=  Some play.Player }

                | { First= firstPlayer; Second=None; Third=None }, Single -> { First=Some play.Player; Second=firstPlayer; Third=None }
                | { First= firstPlayer; Second=None; Third=None }, Double -> { First=None; Second=Some play.Player; Third=firstPlayer }
                | { First= firstPlayer; Second=None; Third=None }, Triple -> { First=None; Second=None; Third=Some play.Player }

                | { First= None; Second=firstPlayer; Third=None }, Single -> { First=Some play.Player; Second=None; Third=firstPlayer }
                | { First= None; Second=firstPlayer; Third=None }, Double -> { First=None; Second=Some play.Player; Third=None }
                | { First= None; Second=firstPlayer; Third=None }, Triple -> { First=None; Second=None; Third=Some play.Player }

                | { First= None; Second=None; Third=firstPlayer }, Single -> { First=Some play.Player; Second=None; Third=None }
                | { First= None; Second=None; Third=firstPlayer }, Double -> { First=None; Second=Some play.Player; Third=None }
                | { First= None; Second=None; Third=firstPlayer }, Triple -> { First=None; Second=None; Third=Some play.Player }

                | { First= secondPlayer; Second=firstPlayer; Third=None }, Single -> { First=Some play.Player; Second=secondPlayer; Third=firstPlayer }
                | { First= secondPlayer; Second=firstPlayer; Third=None }, Double -> { First=None; Second=Some play.Player; Third=secondPlayer }
                | { First= secondPlayer; Second=firstPlayer; Third=None }, Triple -> { First=None; Second=None; Third=Some play.Player }

                | { First= None; Second=secondPlayer; Third=firstPlayer }, Single -> { First=Some play.Player; Second=None; Third=secondPlayer }
                | { First= None; Second=secondPlayer; Third=firstPlayer }, Double -> { First=None; Second=Some play.Player; Third=None }
                | { First= None; Second=secondPlayer; Third=firstPlayer }, Triple -> { First=None; Second=None; Third=Some play.Player }

                | { First= secondPlayer; Second=None; Third=firstPlayer }, Single -> { First=Some play.Player; Second=secondPlayer; Third=None }
                | { First= secondPlayer; Second=None; Third=firstPlayer }, Double -> { First=None; Second=Some play.Player; Third=secondPlayer }
                | { First= secondPlayer; Second=None; Third=firstPlayer }, Triple -> { First=None; Second=None; Third=Some play.Player }

                | _ -> initializedBases // Haven't identified any other cases...

代价:

在编写基于属性的测试时,我在我的函数中发现了一个异常:

当你将同一个玩家分配到多个基地时会发生什么?

我的属性测试确定了这种情况。

问题:

管理输入函数的无效数据的当前做法是什么?

我想我理解在实际系统中,在域模型中点击此函数之前,无效数据应该在验证时失败。

但是,如果某些无效数据如何到达具有严格规则的函数呢?

我是否会抛出异常?

该函数是否应该有其他逻辑来管理异常而不抛出异常?

我的财产测试如下:

module Properties

open Model
open FsCheck
open FsCheck.Xunit

[<Property(QuietOnSuccess = true)>]
let ``Any hit results in a base beeing filled`` () =

    let values = Arb.generate<Play list> |> Gen.suchThat (fun plays -> plays.Length > 0)
                                         |> Arb.fromGen
    Prop.forAll values <| fun plays ->

        // Test
        let actual = plays |> assignBases

        // Verify
        actual <> { First=None; Second=None; Third=None }

失败是这样的:

Test Name:  Properties.Any hit results in a base beeing filled
Test FullName:  Properties.Any hit results in a base beeing filled

Result Message: 
FsCheck.Xunit.PropertyFailedException : 
Falsifiable, after 37 tests (0 shrinks) (StdGen (543307172,296154334)):
Original:
<null>
[{Player = Brian;
  Hit = Double;}; {Player = Scott;
                   Hit = Triple;}; {Player = Cherice;
                                    Hit = Double;}; {Player = Brian;
                                                     Hit = Single;};
 {Player = Cherice;
  Hit = Double;}; {Player = Brian;
                   Hit = Double;}; {Player = Cherice;
                                    Hit = Triple;}; {Player = Brian;
                                                     Hit = Single;};
 {Player = Cherice;
  Hit = Double;}; {Player = Brian;
                   Hit = Single;}; {Player = Cherice;
                                    Hit = Triple;}; {Player = Brian;
                                                     Hit = Single;};
 {Player = Brian;
  Hit = Triple;}; {Player = Cherice;
                   Hit = Triple;}; {Player = Brian;
                                    Hit = Double;}; {Player = Brian;
                                                     Hit = Triple;};
 {Player = Cherice;
  Hit = Triple;}; {Player = Cherice;
                   Hit = Single;}; {Player = Scott;
                                    Hit = Single;}; {Player = Scott;
                                                     Hit = Single;};
 {Player = Scott;
  Hit = Double;}]

失败摘要:

具体来说,我测试中的模式匹配逻辑没有考虑分配给多个基数的同一个玩家。

整个代码在这里:

module Model

(*Types*)
type Position =
    | First
    | Second
    | Third 

type Player =
    | Scott
    | Brian
    | Cherice

type Hit =
    | Single
    | Double
    | Triple

type Play = { Player: Player; Hit: Hit }

type Bases = { 
    First:Player  option
    Second:Player option
    Third:Player  option
}

(*Functions*)
let assignBases (plays:Play list) =

    let initializedBases = { First=None; Second=None; Third=None }

    match plays with
    | [] -> initializedBases
    | _  -> let move bases play = 
                match (bases, play.Hit)  with
                | { First= None; Second=None; Third=None }, Single -> { bases with First=  Some play.Player }
                | { First= None; Second=None; Third=None }, Double -> { bases with Second= Some play.Player }
                | { First= None; Second=None; Third=None }, Triple -> { bases with Third=  Some play.Player }

                | { First= firstPlayer; Second=None; Third=None }, Single -> { First=Some play.Player; Second=firstPlayer; Third=None }
                | { First= firstPlayer; Second=None; Third=None }, Double -> { First=None; Second=Some play.Player; Third=firstPlayer }
                | { First= firstPlayer; Second=None; Third=None }, Triple -> { First=None; Second=None; Third=Some play.Player }

                | { First= None; Second=firstPlayer; Third=None }, Single -> { First=Some play.Player; Second=None; Third=firstPlayer }
                | { First= None; Second=firstPlayer; Third=None }, Double -> { First=None; Second=Some play.Player; Third=None }
                | { First= None; Second=firstPlayer; Third=None }, Triple -> { First=None; Second=None; Third=Some play.Player }

                | { First= None; Second=None; Third=firstPlayer }, Single -> { First=Some play.Player; Second=None; Third=None }
                | { First= None; Second=None; Third=firstPlayer }, Double -> { First=None; Second=Some play.Player; Third=None }
                | { First= None; Second=None; Third=firstPlayer }, Triple -> { First=None; Second=None; Third=Some play.Player }

                | { First= secondPlayer; Second=firstPlayer; Third=None }, Single -> { First=Some play.Player; Second=secondPlayer; Third=firstPlayer }
                | { First= secondPlayer; Second=firstPlayer; Third=None }, Double -> { First=None; Second=Some play.Player; Third=secondPlayer }
                | { First= secondPlayer; Second=firstPlayer; Third=None }, Triple -> { First=None; Second=None; Third=Some play.Player }

                | { First= None; Second=secondPlayer; Third=firstPlayer }, Single -> { First=Some play.Player; Second=None; Third=secondPlayer }
                | { First= None; Second=secondPlayer; Third=firstPlayer }, Double -> { First=None; Second=Some play.Player; Third=None }
                | { First= None; Second=secondPlayer; Third=firstPlayer }, Triple -> { First=None; Second=None; Third=Some play.Player }

                | { First= secondPlayer; Second=None; Third=firstPlayer }, Single -> { First=Some play.Player; Second=secondPlayer; Third=None }
                | { First= secondPlayer; Second=None; Third=firstPlayer }, Double -> { First=None; Second=Some play.Player; Third=secondPlayer }
                | { First= secondPlayer; Second=None; Third=firstPlayer }, Triple -> { First=None; Second=None; Third=Some play.Player }

                | _ -> initializedBases // Haven't identified any other cases...

            (initializedBases, plays) ||> List.fold (fun bases play -> 
                                                         play |> move bases)

更新

根据发布的推荐,我对我的模型进行了一些更新:

我添加了一个Status类型来表示正在处理的数据的状态:

type Status =
    | Valid of Bases
    | Invalid of Play list

然后我将此状态应用于基值,以便将预期状态标记为&#34;有效&#34;和意外标记为&#34;无效&#34;:

let assignBases (plays:Play list) =

    let initializedBases = { First=None; Second=None; Third=None }

    match plays with
    | [] -> Valid initializedBases
    | _  -> let move bases play = 
                match (bases, play.Hit)  with
                | Valid { First= None; Second=None; Third=None }, Single -> Valid { First=  Some play.Player; Second=None; Third=None }
                | Valid { First= None; Second=None; Third=None }, Double -> Valid { First=None; Second= Some play.Player; Third=None }
                | Valid { First= None; Second=None; Third=None }, Triple -> Valid { First=None; Second=None; Third=  Some play.Player }

                | Valid { First= firstPlayer; Second=None; Third=None }, Single -> Valid { First=Some play.Player; Second=firstPlayer; Third=None }
                | Valid { First= firstPlayer; Second=None; Third=None }, Double -> Valid { First=None; Second=Some play.Player; Third=firstPlayer }
                | Valid { First= firstPlayer; Second=None; Third=None }, Triple -> Valid { First=None; Second=None; Third=Some play.Player }

                | Valid { First= None; Second=firstPlayer; Third=None }, Single -> Valid { First=Some play.Player; Second=None; Third=firstPlayer }
                | Valid { First= None; Second=firstPlayer; Third=None }, Double -> Valid { First=None; Second=Some play.Player; Third=None }
                | Valid { First= None; Second=firstPlayer; Third=None }, Triple -> Valid { First=None; Second=None; Third=Some play.Player }

                | Valid { First= None; Second=None; Third=firstPlayer }, Single -> Valid { First=Some play.Player; Second=None; Third=None }
                | Valid { First= None; Second=None; Third=firstPlayer }, Double -> Valid { First=None; Second=Some play.Player; Third=None }
                | Valid { First= None; Second=None; Third=firstPlayer }, Triple -> Valid { First=None; Second=None; Third=Some play.Player }

                | Valid { First= secondPlayer; Second=firstPlayer; Third=None }, Single -> Valid { First=Some play.Player; Second=secondPlayer; Third=firstPlayer }
                | Valid { First= secondPlayer; Second=firstPlayer; Third=None }, Double -> Valid { First=None; Second=Some play.Player; Third=secondPlayer }
                | Valid { First= secondPlayer; Second=firstPlayer; Third=None }, Triple -> Valid { First=None; Second=None; Third=Some play.Player }

                | Valid { First= None; Second=secondPlayer; Third=firstPlayer }, Single -> Valid { First=Some play.Player; Second=None; Third=secondPlayer }
                | Valid { First= None; Second=secondPlayer; Third=firstPlayer }, Double -> Valid { First=None; Second=Some play.Player; Third=None }
                | Valid { First= None; Second=secondPlayer; Third=firstPlayer }, Triple -> Valid { First=None; Second=None; Third=Some play.Player }

                | Valid { First= secondPlayer; Second=None; Third=firstPlayer }, Single -> Valid { First=Some play.Player; Second=secondPlayer; Third=None }
                | Valid { First= secondPlayer; Second=None; Third=firstPlayer }, Double -> Valid { First=None; Second=Some play.Player; Third=secondPlayer }
                | Valid { First= secondPlayer; Second=None; Third=firstPlayer }, Triple -> Valid { First=None; Second=None; Third=Some play.Player }

                | _ -> Invalid plays // Haven't identified any other cases...

            (Valid initializedBases, plays) ||> List.fold (fun bases play -> play |> move bases)

3 个答案:

答案 0 :(得分:4)

抛出异常与返回错误状态取决于您在失败期间要采取的操作以及数据来自何处。关于是否抛出异常的一般问题已经在堆栈溢出上多次讨论过。

如果数据来自用户,则首选返回包含成功/失败的对象。你不想让用户崩溃。如果数据是由系统或开发人员生成的,则首选例外。例外将为您提供可以调查的堆栈转储。更一般地说,如果你的程序进入一个状态,它就无法恢复然后抛出异常。

特别是对于F#,返回选项对于简单的情况就足够了。更复杂的案例需要有歧视的工会,这些工会本身可以包含对象或只是枚举。模式匹配非常适合F#中的错误处理。

答案 1 :(得分:3)

我认为这些问题接近于主观但尚未结束的问题,因此请使用this is what I would do而不是this is the gospel according to F#更多地回答这些问题。

  

函数应该如何处理无效的数据集?

我看待数据的一种方式是它有两个世界,一个来自环境,unsanitized一个已经sanitized并且不应该导致错误。因此,为了从未经过清理的数据中获取数据并进行消毒,请检查它,如果通过则可以使用它,如果不是因为原因而拒绝它。如果你做的一切都是正确的,那么你不应该得到错误。

  

管理输入函数的无效数据的当前做法是什么?

这是一个it depends,但依赖Option type始终是一个很好的第一个后退选项。

更深入的阅读:Simon Peyton Jones的Tackling the Awkward Squad: monadic input/output, concurrency, exceptions, and foreign-language calls in Haskell

阅读完本文后,您将开始了解我为何继续删除Haskell提示。

  

但是,如果某些无效数据如何到达具有严格规则的函数呢?

总之不要让它发生。如果是这样,那不是功能故障而是程序员的故障。这种推理是递归的,所以它会在哪里得到你?

  

我是否会抛出异常?

为什么呢?我每次都可以避免异常,除非我正在做一些文档或者必须与想要异常的东西进行交互。我再次努力保持全部功能。

  

该函数是否有额外的逻辑来管理异常而不抛出异常?

再次避免例外。 F#不是OO而不是OCaml。是的,在OCaml中使用异常是习惯做法,因为F#和OCaml处理异常的方式之间存在显着差异。

正如我在评论中所指出的那样,我看到你的问题的答案是不修补你的功能,而是改变模型,然后改变功能以匹配模型。

答案 2 :(得分:2)

这是编程风格的问题,取决于您正在处理的问题的类型,而http://developers.gigya.com/display/GD/Ratings+and+Reviews是考虑它的好方法。但我相信,在进入错误案例的细节之前,将错误输入划分为以下三类是有用的:

  • 不常见的值,但是问题域的一部分
  • 无效的值,但程序运行后会很快检测到
  • 违反规范但可能无法快速检测到的值

以下是我对待他们的方式:

程序域中的异常值应该被建模为函数正在解决的问题的一部分,即作为区别联合或其他正常返回类型。如果它是问题的一部分,那么函数正在做的其他事情没有真正的区别。

测试将立即显示的无效值并不重要。一旦程序运行,它们的起源将被发现并修复。将注意力集中在这些方面几乎没有什么好处。

细微违反规范绝对关键的案例,可以考虑该计划是否健全。使用failwithinvalidArginvalidOp,断言(如果所有可能导致它们的代码都使用程序集的调试编译进行测试)或其他将终止的异常程序,因为你不知道程序在做什么