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

问题描述:

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

我一直认为异常是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) 

UPDATE:

基于贴,我已经取得了一些更新我的模型建议:

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

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

我再应用此状态的基础值,使得预期的状态被标记为“有效”和意外的被标记为“无效”:

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) 
+0

也许你需要一个规则,说你不能蝙蝠,如果你在基地。或者你可以把它看成两个队列,一个用于打击者,一个用于基地的队员。或者你可以修改玩家拥有更多的状态,而不是基于其中的三个状态。基地,击球,长凳,防守。 –

抛出异常VS返回错误状态取决于什么行动你希望在失败期间以及数据来自何处。关于是否抛出异常的一般问题在堆栈溢出中已经讨论过很多次了。

如果数据来自用户,则返回包含成功/失败的对象是首选。你不想让用户崩溃。如果数据是由系统或开发人员生成的,则优先考虑异常。异常会给你一个你可以调查的堆栈转储。更一般地说,如果你的程序进入一个状态,它不能从中恢复,然后抛出一个异常。

特别针对F#,返回Option对于简单的情况已经足够了。更复杂的案例需要歧视的联合体,它们本身可以包含对象或枚举。模式匹配非常适合F#中的错误处理。

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

函数应该如何处理一组无效的数据?我看数据

的方法之一是,它有两个世界,一个是从环境中来,是unsanitized的一个已经sanitized,不应该导致错误。因此,要将数据从unsanitized转换为sanitized,请检查它,如果它通过,您可以使用它,如果不拒绝它的原因。如果你做的一切正确,那么你不应该得到错误。

目前对无效数据进行管理的做法是什么?

这是一个it depends,但依靠Option type总是一个很好的第一次后退选项。

为了更深入地读:Tackling the Awkward Squad: monadic input/output, concurrency, exceptions, and foreign-language calls in Haskell由西蒙·佩顿 - 琼斯

在阅读完之后你会开始明白我为什么经常掉线哈斯克尔提示。

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

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

我是否会抛出异常?

为什么?每次我都可以避免例外,除非我正在做一些文档或者需要与需要例外的东西进行交互。我再次尝试保留全部功能。

该函数是否应该有额外的逻辑来管理异常而不引发异常?

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

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

+0

感谢Guy Coder。在选择一个答案之前,我会让社群投票选出答案。 –

+0

这是一个很好的方法,但IMO并不会因为通过'failwith','invalidOp'或'invalidArg'来覆盖一些永远不会发生的容易检测到的情况 - 即“无效数据到达”。如果在执行或重构时出现一些小错误,快速失败可以帮助快速找到它。 – Vandroiy

+1

@Vandroiy您是否在暗示[assertions](https://en.wikipedia.org/wiki/Assertion_(software_development))和[Hoare logic](https://en.wikipedia.org/wiki/Hoare_logic)?我也发现它们很有用。正如我们所知道的,没有一个可以接受的答案。 –

这是编程风格的问题,取决于您正在处理的问题的类型,并且Guy Coder's approach是考虑它的好方法。但我相信,再进的错误案件的细节,这是非常有用的错误输入划分为以下三类:

  • 值是不寻常的,但问题域
  • 值是的一部分无效的,但一旦程序运行
  • 值即打破规范,但可能无法进行快速检测

下面是如何对待他们会很快发现:

程序域中的异常值应该被模拟为函数正在解决的问题的一部分,即作为区分联合或其他正常返回类型。如果这是问题的一部分,那么这个功能与其他功能并没有什么实质性的区别。

测试将马上显示的值无效值不重要。程序运行后,将立即找到并修复它们的来源。将注意力集中在这些方面几乎没有什么收获。

微妙违反规范想想如果程序是可靠绝对危重病例。辣椒这些与failwith,invalidArginvalidOp,断言(如果所有可能导致它们的代码都通过程序集的调试编译进行测试)或其他异常即将终止程序,因为你不知道程序是什么正在做什么