Search code examples
reflectionf#immutabilitymutable

F# Mutable to Immutable


Gday All,

I have been dabbling in some F# of late and I came up with the following string builder that I ported from some C# code. It converts an object into a string provided it passes a Regex defined in the attributes. Its probably overkill for the task at hand but its for learning purposes.

Currently the BuildString member uses a mutable string variable updatedTemplate. I have been racking my brain to work out a way doing this without any mutable objects to no avail. Which brings me to my question.

Is it possible to implement the BuildString member function without any mutable objects?

Cheers,

Michael

//The Validation Attribute
type public InputRegexAttribute public (format : string) as this =
    inherit Attribute()
    member self.Format with get() = format

//The class definition
type public Foo public (firstName, familyName) as this =
    [<InputRegex("^[a-zA-Z\s]+$")>]
    member self.FirstName with get() = firstName 

    [<InputRegex("^[a-zA-Z\s]+$")>]
    member self.FamilyName with get() = familyName 

module ObjectExtensions =
    type System.Object with
        member this.BuildString template =
            let mutable updatedTemplate : string  = template
            for prop in this.GetType().GetProperties() do
                for attribute in prop.GetCustomAttributes(typeof<InputRegexAttribute>,true).Cast<InputRegexAttribute>() do
                    let regex = new Regex(attribute.Format)
                    let value = prop.GetValue(this, null).ToString()
                    if regex.IsMatch(value) then
                        updatedTemplate <- updatedTemplate.Replace("{" + prop.Name + "}", value)
                    else
                        raise (new Exception "Regex Failed")
            updatedTemplate

open ObjectExtensions
try
    let foo = new Foo("Jane", "Doe")
    let out = foo.BuildInputString("Hello {FirstName} {FamilyName}! How Are you?")
    printf "%s" out
with | e -> printf "%s" e.Message

Solution

  • I think you can always transform a "for loop over a sequence with only one effect (mutating a local variable)" into code that gets rid of the mutable; here's an example of the general transform:

    let inputSeq = [1;2;3]
    
    // original mutable
    let mutable x = ""
    for n in inputSeq do
        let nStr = n.ToString()
        x <- x + nStr
    printfn "result: '%s'" x
    
    // immutable
    let result = 
        inputSeq |> Seq.fold (fun x n ->
            // the 'loop' body, which returns
            // a new value rather than updating a mutable
            let nStr = n.ToString()
            x + nStr
        ) ""  // the initial value
    printfn "result: '%s'" result
    

    Your particular example has nested loops, so here's an example of showing the same kind of mechanical transform in two steps:

    let inputSeq1 = [1;2;3]
    let inputSeq2 = ["A";"B"]
    
    let Original() = 
        let mutable x = ""
        for n in inputSeq1 do
            for s in inputSeq2 do
                let nStr = n.ToString()
                x <- x + nStr + s
        printfn "result: '%s'" x
    
    let FirstTransformInnerLoopToFold() = 
        let mutable x = ""
        for n in inputSeq1 do
            x <- inputSeq2 |> Seq.fold (fun x2 s ->
                let nStr = n.ToString()
                x2 + nStr + s
            ) x
        printfn "result: '%s'" x
    
    let NextTransformOuterLoopToFold() = 
        let result = 
            inputSeq1 |> Seq.fold (fun x3 n ->
                inputSeq2 |> Seq.fold (fun x2 s ->
                    let nStr = n.ToString()
                    x2 + nStr + s
                ) x3
            ) ""
        printfn "result: '%s'" result
    

    (In the code above, I used the names 'x2' and 'x3' to make scoping more apparent, but you can just use the name 'x' throughout.)

    It may be worthwhile to try to do this same transform on your example code and post your own answer. This won't necessarily yield the most idiomatic code, but can be an exercise in transforming a for loop into a Seq.fold call.

    (That said, in this example the whole goal is mostly an academic exercise - the code with the mutable is 'fine'.)