传递函数以减少重复代码

Pass Function to reduce duplicate code

我正在尝试学习 F#,我觉得我可以编写/重写此代码块以使其更加 "idiomatic" F#,但我不知道如何完成它。

我的简单程序将从 2 个 csv 文件加载值:Skyrim 药水效果列表和 Skyrim 成分列表。一种成分有 4 种效果。一旦我有了成分,我就可以写一些东西来处理它们 - 现在,我只想以一种有意义的方式编写 CSV 加载。

代码

这是我的类型:

type Effect(name:string, id, description, base_cost, base_mag, base_dur, gold_value) =
    member this.Name = name
    member this.Id = id
    member this.Description = description
    member this.Base_Cost = base_cost
    member this.Base_Mag = base_mag
    member this.Base_Dur = base_dur
    member this.GoldValue = gold_value

type Ingredient(name:string, id, primary, secondary, tertiary, quaternary, weight, value) =
    member this.Name = name
    member this.Id = id
    member this.Primary = primary
    member this.Secondary = secondary
    member this.Tertiary = tertiary
    member this.Quaternary = quaternary
    member this.Weight = weight
    member this.Value = value

这里是我解析每个类型的单个逗号分隔字符串的地方:

let convertEffectDataRow (csvLine:string) =
    let cells = List.ofSeq(csvLine.Split(','))
    match cells with
    | name::id::effect::cost::mag::dur::value::_ ->            
        let effect = new Effect(name, id, effect, Decimal.Parse(cost), Int32.Parse(mag), Int32.Parse(dur), Int32.Parse(value))
        Success effect
    | _ -> Failure "Incorrect data format!"


let convertIngredientDataRow (csvLine:string) =
    let cells = List.ofSeq(csvLine.Split(','))
    match cells with
        | name::id::primary::secondary::tertiary::quaternary::weight::value::_ ->
            Success (new Ingredient(name, id, primary, secondary, tertiary, quaternary, Decimal.Parse(weight), Int32.Parse(value)))
        | _ -> Failure "Incorrect data format!"

所以我 觉得 我应该能够构建一个函数来接受这些函数之一或将它们链接起来之类的,这样我就可以递归地遍历CSV 文件并将这些行传递给上面的正确函数。这是我到目前为止尝试过的方法:

type csvTypeEnum = effect=1 | ingredient=2        

let rec ProcessStuff lines (csvType:csvTypeEnum) =
    match csvType, lines with
        | csvTypeEnum.effect, [] -> []
        | csvTypeEnum.effect, currentLine::remaining ->
            let parsedLine = convertEffectDataRow2 currentLine
            let parsedRest = ProcessStuff remaining csvType
            parsedLine :: parsedRest
        | csvTypeEnum.ingredient, [] -> []
        | csvTypeEnum.ingredient, currentLine::remaining ->
            let parsedLine = convertIngredientDataRow2 currentLine
            let parsedRest = ProcessStuff remaining csvType
            parsedLine :: parsedRest
        | _, _ -> Failure "Error in pattern matching"

但这(可以预见)在递归的第二个实例和最后一个模式上有一个编译错误。具体来说,第二次 parsedLine :: parsedRest 出现不编译。这是因为该函数正在尝试 return 一个 Effect 和一个 Ingredient,这显然是行不通的。

现在,我可以编写 2 个完全不同的函数来处理不同的 CSV,但这感觉像是额外的重复。这个可能是一个比我认为的更难的问题,但感觉这应该是相当简单的。

来源

我从本书第4章拿来的CSV解析代码:https://www.manning.com/books/real-world-functional-programming

你当然可以将一个函数传递给另一个函数并使用 DU 作为 return 类型,例如:

type CsvWrapper =
    | CsvA of string
    | CsvB of int

let csvAfunc x =
    CsvA x

let csvBfunc x =
    CsvB x

let csvTopFun x  =
    x 

csvTopFun csvBfunc 5
csvTopFun csvAfunc "x"

关于类型定义,你可以只使用记录,会节省你一些输入:

type Effect = { 
    name:string 
    id: int 
    description: string
}
let eff = {name="X";id=9;description="blah"}
  1. (可选)按照 s952163 的建议,将 Effect 和 Ingredient 转换为记录。
  2. 仔细考虑函数的 return 类型。 ProcessStuff return 是一种情况下的列表,而另一种情况下是单个项目 (Failure)。因此编译错误。
  3. 您没有显示 SuccessFailure 的定义。您可以将结果定义为

    ,而不是一般的成功
    type Result = 
      | Effect of Effect 
      | Ingredient of Ingredient 
      | Failure of string
    

然后下面的代码编译正确:

let convertEffectDataRow (csvLine:string) =
    let cells = List.ofSeq(csvLine.Split(','))
    match cells with
    | name::id::effect::cost::mag::dur::value::_ ->            
        let effect = new Effect(name, id, effect, Decimal.Parse(cost), Int32.Parse(mag), Int32.Parse(dur), Int32.Parse(value))
        Effect effect
    | _ -> Failure "Incorrect data format!"


let convertIngredientDataRow (csvLine:string) =
    let cells = List.ofSeq(csvLine.Split(','))
    match cells with
        | name::id::primary::secondary::tertiary::quaternary::weight::value::_ ->
            Ingredient (new Ingredient(name, id, primary, secondary, tertiary, quaternary, Decimal.Parse(weight), Int32.Parse(value)))
        | _ -> Failure "Incorrect data format!"

type csvTypeEnum = effect=1 | ingredient=2        

let rec ProcessStuff lines (csvType:csvTypeEnum) =
    match csvType, lines with
    | csvTypeEnum.effect, [] -> []
    | csvTypeEnum.effect, currentLine::remaining ->
        let parsedLine = convertEffectDataRow currentLine
        let parsedRest = ProcessStuff remaining csvType
        parsedLine :: parsedRest
    | csvTypeEnum.ingredient, [] -> []
    | csvTypeEnum.ingredient, currentLine::remaining ->
        let parsedLine = convertIngredientDataRow currentLine
        let parsedRest = ProcessStuff remaining csvType
        parsedLine :: parsedRest
    | _, _ -> [Failure "Error in pattern matching"]

csvTypeEnum type 看起来有问题,但我不确定你想要实现什么,所以只是修复了编译错误。

现在您可以通过在需要时将函数作为参数传递来重构代码以减少重复。但始终从类型开始!

由于行类型没有交织到同一个文件中,并且它们指的是不同的 csv 文件格式,我可能不会选择区分联合,而是将处理函数传递给处理文件行的函数行。

就惯用的做事而言,对于这种简单的数据容器,我会使用 Record 而不是标准的 .NET class。记录提供自动相等和比较实现,这在 F# 中很有用。

您可以这样定义它们:

type Effect = {
    Name : string; Id: string; Description : string; BaseCost : decimal; 
    BaseMag : int; BaseDuration : int; GoldValue : int
    }

type Ingredient= {
    Name : string; Id: string; Primary: string; Secondary : string; Tertiary : string; 
    Quaternary : string; Weight : decimal; GoldValue : int
    }

这需要更改转换函数,例如

let convertEffectDataRow (csvLine:string) =
    let cells = List.ofSeq(csvLine.Split(','))
    match cells with
    | name::id::effect::cost::mag::dur::value::_ ->            
        Success  {Name = name; Id = id; Description = effect;  BaseCost = Decimal.Parse(cost); 
                  BaseMag = Int32.Parse(mag); BaseDuration = Int32.Parse(dur); GoldValue = Int32.Parse(value)}
    | _ -> Failure "Incorrect data format!"

希望如何做另一个是显而易见的。

最后,抛开 enum 并简单地用适当的行函数替换它(我还交换了参数的顺序)。

let rec processStuff f lines  =
    match lines with
    |[] -> []
    |current::remaining -> f current :: processStuff f remaining

参数f只是一个应用于每个字符串行的函数。合适的 f 值是我们上面创建的函数,例如 convertEffectDataRow。所以你可以简单地调用 processStuff convertEffectDataRow 来处理效果文件和 processStuff convertIngredientDataRow 来处理和配料文件。

但是,现在我们简化了 processStuff 函数,我们可以看到它的类型为:f:('a -> 'b) -> lines:'a list -> 'b list。这与内置的 List.map function 相同,因此我们实际上可以完全删除此自定义函数并仅使用 List.map.

let processEffectLines lines = List.map convertEffectDataRow lines

let processIngredientLines lines = List.map convertIngredientDataRow lines