views:

514

answers:

4

I have been dabbling with F# in Visual Studio 2010. I am a developer with more code/architecture design experience in object-oriented languages such as C# and Java.

To expand my skill set and help make better decisions I am trying different languages to do different things. In particular get the hang of coding "correctly" using functional languages (in this case F#).

A simple example is generating some XML, then adding some filters to eliminate some elements.

Here is my code:

open System
open System.Xml.Linq


let ppl:(string * string) list = [
    ("1", "Jerry"); 
    ("2", "Max"); 
    ("3", "Andrew");
]

/// Generates a Person XML Element, given a tuple.
let createPerson (id:string, name:string) = new XElement(XName.Get("Person"),
                                                new XAttribute(XName.Get("ID"), id),
                                                new XElement(XName.Get("Name"), name)
)

/// Filter People by having odd ID's
let oddFilter = fun (id:string, name:string) -> (System.Int32.Parse(id) % 2).Equals(1) 

/// Open filter which will return all people
let allFilter = fun (id:string, name:string) -> true

/// Generates a People XML Element.
let createPeople filter = new XElement(XName.Get("People"), 
                                ppl |> List.filter(filter)  |> List.map createPerson
)

/// First XML Object
let XmlA = createPeople oddFilter

/// Second XML Object
let XmlB = createPeople allFilter


printf "%A\n\n%A" XmlA XmlB


/// Waits for a keypress
let pauseKey = fun () -> System.Console.ReadKey() |> ignore


pauseKey()

My questions are: What things have I done well in this scenario? What parts could be done better?

I am really looking forward to some ideas and I am quite excited about becoming familiar with functional paradigms too! :)

Thanks in advance

+4  A: 
let createPeople filter = new XElement(XName.Get("People"), 
                            ppl |> List.filter(filter)  |> List.map createPerson
)

This part can be deforested manually, or you can hope that the compiler will deforest it for you.

Basically, there is an intermediate structure (the list of filtered people) that, if this is compiled naively, will be allocated to serve only once. Better apply createPerson on each element as it is decided if they are in or out, and build the final result directly.

EDIT: cfern contributed this deforested version of createPeople:

let createPeople filter = 
  new XElement(
    XName.Get("People"), 
    List.foldBack 
      (fun P acc -> if filter P then (createPerson P)::acc else acc) 
      ppl 
      [])

NOTE: because there could be side-effects in filter or createPerson, in F# it is rather hard for the compiler to decide to deforest by itself. In this case it seems to me that deforesting is correct because even if filter has side-effects, createPerson doesn't but I'm no specialist.

Pascal Cuoq
I can't test this right now, but could you replace List.filter and List.map by Seq.filter and Seq.map to avoid creating intermediate lists?
cfern
@cfern If you still do a `filter` followed by a `map`, there is still an intermediate structure that is allocated, used once, and then discarded. I was thinking more along the line of replacing them both by a single `fold` that applies `createPerson` and adds the result to the accumulator only if `filter` tests positive.
Pascal Cuoq
Ah, I see. I'm still new to functional programming, and therefore not yet fully used to folds. Is this what you're thinking of? ... let createPeople filter = new XElement(XName.Get("People"), List.foldBack (fun P acc -> if filter P then (createPerson P)::acc else acc) ppl []) ... where I used foldBack to preserve the order of people.
cfern
@cfern Yes, this is what I am talking about. Sorry not to have provided it myself, but I can only read F#, not write it (OCaml programmer myself).
Pascal Cuoq
Thanks for that Pascal, I will look into deforesting (is that correct?) and the foldBack. It appears there are some List methods I need to be familiar with. :)
Russell
Eh come on - Nobody will understand this on the first sight. Please write a new function `filterMap` for this purpose.
Dario
@Dario 1) Nobody will understand **what**? 2) As I said earlier, I'm not confident enough to write F# 3) This answer is community wiki since cfern proposed the actual alternative implementation, so be our guest. If you were talking about that implementation, I understood it, and I don't even know F#!
Pascal Cuoq
+11  A: 

In principle, your code is all right.

There are just some points that can be simplified from a syntactical point of view.

let ppl:(string * string) list = [
    ("1", "Jerry"); 
    ("2", "Max"); 
    ("3", "Andrew");
]

The compiler is able to deduce most types by himself:

let ppl = [ "1", "Jerry";
            "2", "Max";
            "3", "Andrew" ]

And of course you can re-write your filters like this due to currying:

let oddFilter (id:string, name:string) = (int id) % 2 = 1
let allFilter (id:string, name:string) = true

The biggest improvement would be separating the indices from the names and let the programm do the numbering. You don't have to work with strings instead of numbers and can use more idiomatic tuple-free functions:

let ppl = [ "Jerry"; "Max"; "Andrew" ]

let oddFilter id name = id % 2 = 1
let allFilter id name = true

let createPerson id name = ...

The part

ppl |> List.filter(filter)  |> List.map createPerson

would be rewritten to

[ for (index, name) in List.mapi (fun i x -> (i, x)) do
      if filter index name then
          yield createPerson (string index) name ]
Dario
Yup, type inference and currying. Good stuffs.
cfern
Thanks Dario, there are some good ideas. Do you mind if I ask why you would rewrite the ppl |> List.filter(filter) |> List.map createPerson? I am a bit confused by the processing. Looks like there are some topics I will be looking into. :) Thx again
Russell
The point is only to deal with the indices where they're really needed. So I generate them *inside* `createPeople`.
Dario
`List.choose (item -> if filter item then Some (createPerson item) else None)` works too :) Choose == filter + map at the same time.
Juliet
Yep, choose is cool ... Haven't thought about it.
Dario
Thanks Juliet, the choose function looks great :)
Russell
@Dario - Do you mind if I ask how are the filters you wrote a result of currying?
Russell
Because `let f = fun x -> ...` and `let f x = ...` are the same.
Dario
Ok, Thanks :) .
Russell
+2  A: 

Most of the time deforesting without a specific reason, generally performance, is a bad idea. Which one of these do you see as easier to read and less error prone? Deforesting taken out of context just adds complexity and/or coupling to your code.

let createPeople filter ppl =
    ppl 
    |> List.mapi (fun i x -> (i, x)) 
    |> List.filter filter
    |> List.map createPerson

let createPeople filter ppl =
    [ for (index, name) in ppl |> List.mapi (fun i x -> (i, x)) do
          if filter (index, name) then
              yield createPerson (index, string) ]

let createPeople filter ppl = 
    (ppl |> List.mapi (fun i x -> (i, x)), []) 
    ||> List.foldBack (fun P acc -> if filter P then (createPerson P)::acc else acc)

Once you get used to the syntax function composition allows you to drop ppl.

let createPeople filter =
    List.mapi (fun i x -> (i, x)) 
    >> List.filter filter
    >> List.map createPerson

All of these use tupled data.

let filter (id, name) = 
    id % 2 = 1

let allFilter (id, name) = 
    true

let createPerson (id, name) = 
    ()
gradbot
thanks gradbot. Out of curiosity, what does >> mean in F#? Is this a piping operator like |>? Or is it a bitwise operator (moving bits to the right)?
Russell
It joins two functions together with the function on the left being called first. It is defined as let (>>) f g x = g(f x)
TimothyP
A: 

I have also recently needed to transform an XSL to an XML file. This is the F# I used to do this.

Has some interesting quirks with using the .net methods.

(* Transforms an XML document given an XSLT. *)

open System.IO
open System.Text
open System.Xml
open System.Xml.Xsl

let path = @"C:\\XSL\\"

let file_xml = path + "test.xml"
let file_xsl = path + "xml-to-xhtml.xsl"

(* Compile XSL file to allow transforms *)
let compile_xsl (xsl_file:string) = new XslCompiledTransform() |> (fun compiled -> compiled.Load(xsl_file); compiled)
let load_xml (xml_file:string) = new XmlDocument() |> (fun doc -> doc.Load(xml_file); doc)

(* Transform an Xml document given an XSL (compiled *)
let transform (xsl_file:string) (xml_file:string) = 
      new MemoryStream()
        |> (fun mem -> (compile_xsl xsl_file).Transform((load_xml xml_file), new XmlTextWriter(mem, Encoding.UTF8)); mem)
        |> (fun mem -> mem.Position <- (int64)0; mem.ToArray())

(* Return an Xml fo document that has been transformed *)
transform file_xsl file_xml
    |> (fun bytes -> File.WriteAllBytes(path + "out.html", bytes))
Russell