2016-08-09 41 views
1

Ich versuche, F # zu lernen, und ich fühle mich wie ich diesen Codeblock schreiben/umschreiben kann, um "idiomatischer" F # zu sein, aber ich kann einfach nicht herausfinden, wie ich es erreichen kann.Pass-Funktion, um doppelten Code zu reduzieren

Mein einfaches Programm lädt Werte aus 2 CSV-Dateien: Eine Liste der Skyrim-Trank-Effekte und eine Liste der Skyrim-Zutaten. Eine Zutat hat 4 Effekte. Sobald ich die Zutaten habe, kann ich etwas schreiben, um sie zu verarbeiten - im Moment möchte ich nur die CSV-Ladung in einer Weise schreiben, die Sinn macht.

-Code

Hier sind meine Typen:

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 

Hier ist, wo ich eine einzelne kommagetrennte Zeichenfolge analysieren, pro Typ:

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

So Ich fühle mich Ich sollte in der Lage sein, eine Funktion zu bauen, die eine dieser Funktionen akzeptiert oder sie kettet hing, damit ich rekursiv die Zeilen in der CSV-Datei durchgehen und diese Zeilen an die oben angegebene Funktion übergeben kann. Hier ist, was ich versucht habe, so weit:

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" 

Aber diese (vorhersagbar) hat einen Compiler-Fehler auf der zweiten Instanz von Rekursion und das letzte Muster. Genauer gesagt, das zweite Mal parsedLine :: parsedRest zeigt sich nicht kompilieren. Dies liegt daran, dass die Funktion versucht, sowohl Effect als auch Ingredient zurückzugeben, was offensichtlich nicht funktioniert.

Jetzt könnte ich nur 2 völlig verschiedene Funktionen schreiben, um die verschiedenen CSVs zu behandeln, aber das fühlt sich an wie doppelte Vervielfältigung. Diese könnte ein härteres Problem sein, als ich es Kredit gebe, aber es fühlt sich an, als sollte das ziemlich direkt sein.

Quellen

Der Code CSV-Parsing ich aus Kapitel 4 dieses Buches nahm: https://www.manning.com/books/real-world-functional-programming

Sie
+1

Möchten Sie dies selbst implementieren oder suchen Sie nur nach einer Lösung? Der schnellste Weg wäre, den [CSV Type Provider] (http://fsharp.github.io/FSharp.Data/library/CsvProvider.html) zu verwenden. Sie sollten keine Enumeration definieren müssen, sondern nur Effekt und Inhaltsstoff in CsvType einbinden. – s952163

Antwort

2

Da die Linientypen nicht in die gleiche Datei verschachtelt sind und sich auf verschiedene CSV-Dateiformate beziehen, würde ich wahrscheinlich keine diskriminierte Union wählen und stattdessen die Verarbeitungsfunktion an die Funktion übergeben, die die Datei zeilenweise verarbeitet .

In Bezug auf Dinge idiomatisch zu tun, würde ich eine Record anstelle einer Standard-.NET-Klasse für diese Art von einfachen Datencontainer verwenden. Datensätze bieten automatische Gleichheits- und Vergleichsimplementierungen, die in F # nützlich sind.

Sie können sie wie folgt definieren:

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 
    } 

, dass eine Änderung der Umwandlungsfunktion erfordert, z.B.

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

Hoffentlich ist es offensichtlich, wie man das andere tut.

Schließlich beiseite die enum und ersetzen Sie es einfach durch die entsprechende Linienfunktion (Ich habe auch die Reihenfolge der Argumente ausgetauscht).

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

Das Argument f ist nur eine Funktion, die für jede Zeichenfolge Leitung angelegt wird. Geeignete f Werte sind die Funktionen, die wir oben erzeugt haben, z. convertEffectDataRow. So können Sie einfach processStuff convertEffectDataRow aufrufen, um eine Effektdatei zu verarbeiten und processStuff convertIngredientDataRow verarbeiten und Zutaten-Datei.

Allerdings haben wir jetzt die processStuff Funktion vereinfacht, können wir sehen, es hat Typ: f:('a -> 'b) -> lines:'a list -> 'b list. Dies ist das gleiche wie das integrierte List.map function, so dass wir diese benutzerdefinierte Funktion tatsächlich vollständig entfernen können und einfach List.map verwenden.

let processEffectLines lines = List.map convertEffectDataRow lines 

let processIngredientLines lines = List.map convertIngredientDataRow lines 
+2

Vielen Dank für die tolle Schritt-für-Schritt! Ich wusste nicht, dass Sie eine Funktion übergeben können, ohne die Argumente oder den Rückgabetyp zu deklarieren (wie Sie es bei f gemacht haben), was dieses Problem sicherlich viel einfacher macht. – Max

0

sicherlich eine Funktion an eine andere Funktion und verwenden Sie einen DU als Rückgabetyp, beispielsweise passieren kann:

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" 

Was die Typdefinitionen können Sie nur Datensätze verwenden, werden Sie etwas Tipp sparen:

type Effect = { 
    name:string 
    id: int 
    description: string 
} 
let eff = {name="X";id=9;description="blah"} 
2
  1. (optional) Konvertiere Effekt und Inhaltsstoffe in Datensätze, wie s952163 vorgeschlagen.
  2. Denken Sie sorgfältig über die Rückgabetypen Ihrer Funktionen nach. ProcessStuff gibt eine Liste von einem Fall, aber einen einzelnen Artikel (Failure) aus dem anderen Fall zurück. Also Kompilierungsfehler.
  3. Sie haben nicht gezeigt, was Success und Failure Definitionen sind.Statt generic Erfolg, könnten Sie das Ergebnis als

    type Result = 
        | Effect of Effect 
        | Ingredient of Ingredient 
        | Failure of string 
    

definieren und dann der folgende Code kompiliert korrekt:

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 Typ fischig aussieht, aber ich bin nicht sicher, was Sie waren versuchen zu erreichen, also nur die Kompilierungsfehler behoben.

Jetzt können Sie Ihren Code neu gestalten, um die Duplizierung zu reduzieren, indem Sie bei Bedarf Funktionen als Parameter übergeben. Beginne aber immer mit Typen!