递归函数中的空引用异常

Null Reference Exception in Recursive Function

下面的代码来自https://www.manning.com/books/real-world-functional-programming

的第8章

当我 运行 代码时,我在 testClientTree 中收到空引用异常。我检查了本书的勘误表,但没有找到任何内容。

type Client =
  { Name : string
    Income : int
    YearsInJob : int
    UsesCreditCard : bool
    CriminalRecord : bool }

let john = 
  { Name = "John Doe"  
    Income = 40000
    YearsInJob = 1
    UsesCreditCard = true
    CriminalRecord = false }

type ClientTests = 
  { Check   : Client -> bool
    Report : Client -> unit }

type QueryInfo =
  { Title : string
    Test : Client -> bool
    Positive : Decision
    Negative : Decision }
    
and Decision = 
  | Result of string  
  | Query of QueryInfo

let rec tree = 
    Query({ Title = "More than k" 
            Test = (fun cl -> cl.Income > 40000)
            Positive = moreThan40; Negative = lessThan40 })
and moreThan40 = 
    Query({ Title = "Has criminal record"
            Test = (fun cl -> cl.CriminalRecord)
            Positive = Result("NO"); Negative = Result("YES") })
and lessThan40 = 
    Query({ Title = "Years in job"
            Test = (fun cl -> cl.YearsInJob > 1)
            Positive = Result("YES"); Negative = usesCredit })
and usesCredit = 
    Query({ Title = "Uses credit card"
            Test = (fun cl -> cl.UsesCreditCard)
            Positive = Result("YES"); Negative = Result("NO") })

let rec testClientTree(client, tree) =
  match tree with
  | Result(msg) ->
      printfn "  OFFER A LOAN: %s" msg
  | Query(qi) ->
      let s, case = if (qi.Test(client)) then "yes", qi.Positive
                    else "no", qi.Negative
      printfn "  - %s? %s" qi.Title s
      testClientTree(client, case)

[<EntryPoint>]
let main argv =
    testClientTree(john, tree)
    0

不知道书上为什么会有这个初始化代码,但是失败的原因是数据实际上没有初始化。

您需要这样做:

let rec tree = 
    Query({ Title = "More than k" 
            Test = (fun cl -> cl.Income > 40000)
            Positive = 
                Query({ Title = "Has criminal record"
                        Test = (fun cl -> cl.CriminalRecord)
                        Positive = Result("NO")
                        Negative = Result("YES") })
            Negative = 
                Query({ Title = "Years in job"
                        Test = (fun cl -> cl.YearsInJob > 1)
                        Positive = Result("YES");
                        Negative = 
                            Query({ Title = "Uses credit card"
                                    Test = (fun cl -> cl.UsesCreditCard)
                                    Positive = Result("YES"); Negative = Result("NO") })})})

出于这样的原因,如果可以的话,最好不要定义一堆相互递归的东西。

首先,这是一个 F# 编译器错误 Initialization of mutually recursive values can sometimes leave fields uninitialized #6750

你可以用它做什么?

  1. 不要使用相互递归的值。使用 let 绑定并按照 Bent 的建议重新排序(我会这样做)

  2. 不要使用相互递归的值。按照菲利普的建议使用一个大的初始化

  3. 延迟定义 lazy 表达式

  4. 使用引用递归定义的值的函数表达式

在我们跳到示例之前,我建议更改 Decision 的定义以使结果明确并删除字符串:

Decision = 
  | Approved
  | Declined
  | Query of QueryInfo

此外,我会建议根据查询的内容来命名查询,而不是根据导致新查询的先前查询的部分来命名查询。 IE。而不是 lessThan40 使用 checkYearsInJob.

所以,最简单的方法 - 重新排序 let 绑定

let checkCriminalRecord =
    Query { Title = "Has criminal record"
            Test = fun cl -> cl.CriminalRecord
            Positive = Declined
            Negative = Approved }
let checkCreditCardUsage =
    Query { Title = "Uses credit card"
            Test = fun cl -> cl.UsesCreditCard
            Positive = Approved
            Negative = Declined }
let checkYearsInJob =
    Query { Title = "Years in job"
            Test = fun cl -> cl.YearsInJob > 1
            Positive = Approved
            Negative = checkCreditCardUsage }
let checkIncome =
    Query { Title = "More than k"
            Test = fun cl -> cl.Income > 40000
            Positive = checkCriminalRecord
            Negative = checkYearsInJob }

使用 lazy 延迟(请注意,Positive 和 Negative 字段都应声明为 Lazy<Decision>

let rec checkIncome =
    Query { Title = "More than k"
            Test = fun cl -> cl.Income > 40000
            Positive = lazy checkCriminalRecord
            Negative = lazy checkYearsInJob }
and checkCriminalRecord =
    Query { Title = "Has criminal record"
            Test = fun cl -> cl.CriminalRecord
            Positive = lazy Declined
            Negative = lazy Approved }
and checkYearsInJob =
    Query { Title = "Years in job"
            Test = fun cl -> cl.YearsInJob > 1
            Positive = lazy Approved
            Negative = lazy checkCreditCardUsage }
and checkCreditCardUsage =
    Query { Title = "Uses credit card"
            Test = fun cl -> cl.UsesCreditCard
            Positive = lazy Approved
            Negative = lazy Declined }

测试树时强制一个case执行一个值

testClientTree(client, case.Force())

最后,使用函数。在这里,我会将查询定义更改为 return decision directly from test:

type QueryInfo =
  { Title : string
    Test : Client -> Decision }

您仍然可以 return 测试的布尔结果,但专注于贷款报价可以使代码更清晰:

let rec checkIncome =
    Query { Title = "More than k"
            Test = fun c -> if c.Income > 40000 then checkCriminal else checkYearsInJob }
and checkCriminal =
    Query { Title = "Has criminal record"
            Test = fun c -> if c.CriminalRecord then Declined else Approved }
and checkYearsInJob =
    Query { Title = "Years in job"
            Test = fun c -> if c.YearsInJob > 1 then Approved else checkCreditCardUsage }
and checkCreditCardUsage =
    Query { Title = "Uses credit card"
            Test = fun c -> if c.UsesCreditCard then Approved else Declined }