【问题标题】:Pass Function to reduce duplicate code传递函数以减少重复代码
【发布时间】:2016-08-09 00:06:41
【问题描述】:

我正在尝试学习 F#,我觉得我可以编写/重写这段代码以使其更“惯用”F#,但我就是不知道如何完成它。

我的简单程序将从 2 个 csv 文件加载值:天际药水效果列表和天际成分列表。一种成分有 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 第二次出现不会编译。这是因为该函数试图同时返回一个 Effect 和一个 Ingredient,这显然不会。

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

来源

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

【问题讨论】:

  • 您想自己实现还是只是在寻找解决方案?最快的方法就是使用CSV Type Provider。您不需要定义枚举,只需将 Effect 和 Ingredient 包装到 CsvType 中即可。

标签: csv parsing f# encapsulation


【解决方案1】:

由于行类型没有交错到同一个文件中,并且它们引用不同的 csv 文件格式,我可能不会使用有区别的联合,而是将处理函数传递给逐行处理文件的函数。

就惯用的做事方式而言,对于这种简单的数据容器,我会使用Record 而不是标准的.NET 类。记录提供了在 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

【讨论】:

  • 感谢您的精彩一步一步!我没有意识到你可以在不声明参数或返回类型的情况下传递一个函数(就像你对 f 所做的那样),这无疑使这个问题变得容易得多。
【解决方案2】:
  1. (可选)按照 s952163 的建议将效果和成分转换为记录。
  2. 仔细考虑函数的返回类型。 ProcessStuff 从一种情况返回一个列表,但从另一种情况返回单个项目 (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 类型看起来很可疑,但我不确定您要达到什么目的,所以只修复了编译错误。

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

【讨论】:

    【解决方案3】:

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

    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"}
    

    【讨论】:

      猜你喜欢
      • 2017-08-25
      • 2013-01-28
      • 1970-01-01
      • 1970-01-01
      • 2021-07-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多