我正在尝试学习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
,显然不会这样做。
现在,我可以编写两个完全不同的函数来处理不同的CSV,但这感觉就像是额外的重复。这个可能是一个比我更有信心的难题,但感觉这应该是相当简单的。
来源
我从本书第4章中获取的CSV解析代码:https://www.manning.com/books/real-world-functional-programming
答案 0 :(得分:2)
ProcessStuff
从一个案例中返回一个列表,但从另一个案例中返回一个项目(Failure
)。因此编译错误。您尚未显示Success
和Failure
定义。您可以将结果定义为
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
类型看起来很可疑,但我不确定你想要实现什么,所以只修复了编译错误。
现在,您可以通过在需要时将函数作为参数传递来重构代码以减少重复。但总是从类型开始!
答案 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"}