传递函数以减少重复代码

时间:2016-08-09 00:06:42

标签: csv parsing f# encapsulation

我正在尝试学习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显示不编译。这是因为该函数试图同时返回EffectIngredient,显然不会这样做。

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

来源

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

3 个答案:

答案 0 :(得分:2)

  1. (可选)将效果和成分转换为记录,如s952163建议的那样。
  2. 仔细考虑功能的返回类型。 ProcessStuff从一个案例中返回一个列表,但从另一个案例中返回一个项目(Failure)。因此编译错误。
  3. 您尚未显示SuccessFailure定义。您可以将结果定义为

    ,而不是通用成功
    type Result = 
      | Effect of Effect 
      | Ingredient of Ingredient 
      | Failure of string
    
  4. 然后以下代码正确编译:

    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类型看起来很可疑,但我不确定你想要实现什么,所以只修复了编译错误。

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

答案 1 :(得分:2)

由于行类型没有交错到同一个文件中并且它们引用不同的csv文件格式,我可能不会选择Discriminated Union而是将处理函数传递给处理文件行的函数线。

就习惯用法而言,我会使用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

答案 2 :(得分:0)

您当然可以将函数传递给另一个函数并使用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"}