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

72 views Asked by At

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.

1

There are 1 answers

3
Brian Berns On BEST ANSWER

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

Related Questions in F#