Working with .NET keywords in F #?

  • I am not a functional programmer.
  • I am learning F #.
  • I have a problem here.

Let me start with the following piece of code:

type XmlNode(tagName, innerValue) =
    member this.TagName = tagName
    member this.InnerValue = innerValue
    member this.Atts = Dictionary<string, obj>()

      

I am not using F # dict

because (as I know) it is read-only, however I obviously need to change my attributes. So I'm really trying to do it in a clean functional way:

type XmlNode with member this.WriteTo (output:StringBuilder) = 
    output.Append("<" + this.TagName) |> ignore
    //let writeAtts = 
    //    List.map2 (fun key value -> " " + key + "=" + value.ToString())
 (List.ofSeq this.Atts.Keys) (List.ofSeq this.Atts.Values)
    //    |> List.reduce (fun acc str -> acc + " " + str)
    //output.Append((writeAtts)) |> ignore
    output.Append(">" + this.InnerValue + "</" + this.TagName + ">") |> ignore
    output

      

The code I commented was my (possibly silly) attemp to use mapping and shortening to concatenate all atts in a single correctly formatted line. And it compiles OK.

But when I try to access the Atts property:

[<EntryPoint>]
let main argv = 
    let root = new XmlNode("root", "test")
    root.Atts.Add("att", "val") // trying to add a new KVP
    let output = new StringBuilder()
    printfn "%O" (root.WriteTo(output))
    Console.ReadLine()|>ignore
    0 // return an integer exit code

      

... the new attribute does not appear inside the Atts property, i.e. it remains empty.

So: 1) help me make my code more functional. 2) and understand how to deal with modifiable dictionaries in F #.

Thank.

+3


source to share


1 answer


First, your immediate problem: the way a property is defined Atts

is not a single value that is "stored" somewhere and accessed through the property. Instead, your definition means "every time someone reads this property, create a new dictionary and return it." This is why your new attribute doesn't appear in the dictionary: it root.Atts

is a different dictionary every time you read .

To create a property using a backing field and an initial value, use member val

:

type XmlNode(...) =
   ...
   member val Atts = Dictionary<string,object>()

      

Now, answers to some of the implied questions.

First order of business: "change attributes" and "purely functional" are conflicting ideas. Functional programming implies immutable data. Nothing changes. The way to advance your computation is to create a new database at every step, without overwriting the previous one. This basic idea turns out to be extremely valuable in practice: safer threading, trivial "undo" scenarios, trivial parallelization, trivial propagation to other machines, and even reduced memory consumption through persistent data structures.

Inevitability is a very important point, and I urge you not to look into it. Accepting this requires a mental shift. From my own (and other people I know) experiences, it's very difficult to come from imperative programming, but it's worth it.

Second, don't use classes and properties. Technically speaking, object-oriented programming (in the sense of messaging) does not contradict functionality, but the taste of the enterprise, which is used in practice and implemented in C ++, Java, C #, etc., is contradictory as it emphasizes this idea that "methods - these are operations that change the state of the object ", which does not work (see above). Therefore, it is best to avoid OO constructs, at least while you are learning. Moreover, F # provides much better ways to encode data:

type XmlNode = { TagName: string; InnerValue: string; Atts: (string*string) list }

      

(note that mine is Atts

not a dictionary, we will reflect a little)

Likewise, to represent operations on your data, use functions rather than methods:

 let printNode (node: XmlNode) = (* we'll come to the implementation later *)

      



Third, why are you saying that you "obviously" need to change the attributes? The code you provided does not require this. For example, using my definition XmlNode

above, I can rewrite the code this way:

[<EntryPoint>]
let main argv = 
    let root = { TagName = "root"; InnerValue = "test"; Atts = ["att", "val"] }
    printfn "%s" (printNode root)
    ...

      

But even if it was a real need, you should not do it "on the spot." As I said above, when you talk about immutability, you should not mutate an existing node, but rather create a new node that differs from the original in that you wanted to "change":

let addAttr node name value = { node with Atts = (name, value) :: node.Atts }

      

In this implementation, I take a node and attribute name / value and create a new node whose list Atts

consists of what was in the original node Atts

, with the new attribute prefixed.

The original Atts

list remains unchanged, unmodified. But this does not mean twice as much memory consumption: since we know that the original list never changes, we can reuse it: we create a new list, allocating only memory for the new element and include the reference to the old list as "other elements" ... If the old list could be changed, we could not, we would need to create a complete copy (see Defensive copy. ") This strategy is called" Persistent data structure. "This is one of the pillars of functional programming.

Finally, for formatting strings, I recommend using sprintf

instead StringBuilder

. It offers similar operational benefits, but also provides type safety. For example, the code sprintf "%s" 5

won't compile complaining that the format expects a string, but the final argument 5

is a number. With this, we can implement the function printNode

:

 let printNode (node: XmlNode) =
    let atts = seq { for n, v in node.Atts -> sprintf " %s=\"%s\"" n v } |> String.concat ""
    sprintf "<%s%s>%s</%s>" node.TagName atts node.InnerValue node.TagName

      

For reference, here's your complete program rewritten in functional style:

type XmlNode = { TagName: string; InnerValue: string; Atts: (string*string) list }

let printNode (node: XmlNode) =
    let atts = seq { for n, v in node.Atts -> sprintf " %s=\"%s\"" n v } |> String.concat ""
    sprintf "<%s%s>%s</%s>" node.TagName atts node.InnerValue node.TagName

[<EntryPoint>]
let main argv = 
   let root = { TagName = "root"; InnerValue = "test"; Atts = ["att", "val"] }
   printfn "%s" (printNode root)

   Console.ReadLine() |> ignore
   0

      

+5


source







All Articles