typesf#docxcode-duplication

Remove the duplication of code which is exactly the same for two discrete types in F#


I have a discrete union WordContainer that is either a Doc of WordDocument or a Cell of WordTableCell. For the purposes of this specific function each type has the same API in terms of functions. I am using a match expression to determine whether the value is a Doc or a Cell and then have under each case the same code with two different variables.

I have spent some time looking this up and reading documentation to figure out how to remove the duplication. First I thought a type class would be great but those are apparently a foreign concept to F#. Then I thought that maybe an interface could be good, but it seemed like I needed full blown classes for those using actual methods which greatly complicated the type hierarchy that I have right now. Trying to leave the type wrapped and working through that did not work, didn't expect it to but I still tried.


Solution

  • The problem here is that WordTableCell and WordDocument offer similar API's, but don't share a base class or interface. This is called "duck typing".

    Solution 1: Helper functions

    One way to handle this is to factor out the differences by creating your own addParagraph and addTable helper functions:

        let rec addPartUnified (part: DocumentPart) addParagraph addTable =
            match part with
            | Par paragraph ->
                for run in paragraph do
                    let wordParagraph : OfficeIMO.Word.WordParagraph = addParagraph run.text
                    ()
            | Img image ->
                (addParagraph "").AddImage (image.path, 100, 100, OfficeIMO.Word.WrapTextImage.Square) |> ignore
            | Tab table ->
                let wordTable : OfficeIMO.Word.WordTable = addTable (table.rows, table.cols)
                for cell in table.cells do
                    for row = cell.height - 1 downto 0 do
                        for col = cell.width - 1 downto 0 do
                            wordTable.Rows[row+cell.y].Cells[col+cell.x].MergeHorizontally 1
                            wordTable.Rows[row+cell.y].Cells[col+cell.x].MergeVertically 1
                    FillWordContainer (Cell wordTable.Rows[cell.y].Cells[cell.x]) cell.content
            | Lst listing ->
                let wordListing = (addParagraph "").AddList OfficeIMO.Word.WordListStyle.Bulleted
                for text in listing do
                    wordListing.AddItem text |> ignore
    

    You can then call the unified function like this:

        and addPart part (master: WordContainer) =
            match master with
            | Cell cell ->
                addPartUnifiied part cell.AddParagraph cell.AddTable
            | Doc document ->
                addPartUnifiied part document.AddParagraph document.AddTable
    

    This is basically a poor man's typeclass.

    Solution 2: Members

    Another solution that is slightly more verbose, but perhaps more extensible, is to create a unified interface on the WordContainer type by defining AddParagraph and AddTable members:

        type WordContainer =
        
            Doc of OfficeIMO.Word.WordDocument | Cell of OfficeIMO.Word.WordTableCell
    
            member master.AddParagraph(text : string) =
                match master with
                | Cell cell -> cell.AddParagraph(text)
                | Doc document -> document.AddParagraph(text)
    
            member master.AddTable(rows, columns) =
                match master with
                | Cell cell -> cell.AddTable(rows, columns)
                | Doc document -> document.AddTable(rows, columns)
    

    You can then call them like this:

        let rec addPart (part: DocumentPart) (master: WordContainer) =
            match part with
            | Par paragraph ->
                for run in paragraph do
                    let wordParagraph = master.AddParagraph run.text
                    ()
            | Img image ->
                (master.AddParagraph "").AddImage (image.path, 100, 100, OfficeIMO.Word.WrapTextImage.Square) |> ignore
            | Tab table ->
                let wordTable = master.AddTable (table.rows, table.cols)
                for cell in table.cells do
                    for row = cell.height - 1 downto 0 do
                        for col = cell.width - 1 downto 0 do
                            wordTable.Rows[row+cell.y].Cells[col+cell.x].MergeHorizontally 1
                            wordTable.Rows[row+cell.y].Cells[col+cell.x].MergeVertically 1
                    FillWordContainer (Cell wordTable.Rows[cell.y].Cells[cell.x]) cell.content
            | Lst listing ->
                let wordListing = (master.AddParagraph "").AddList OfficeIMO.Word.WordListStyle.Bulleted
                for text in listing do
                    wordListing.AddItem text |> ignore