FSharpN00b
FSharpN00b

Reputation: 943

F#: Class with no default constructor as type parameter?

I have a FileReader class whose job is to read and process text files using a StreamReader. To facilitate unit testing, I'd like to provide a type parameter to this class so that I can swap the StreamReader for a FakeReader that doesn't actually interact with the file system (and maybe throws exceptions such as OutOfMemory, so I can test the error handling in FileReader).

Ideally, I'd like to define FileReader something like this (trivialized for clarity):

type FileReader<'Reader> =
    member this.Read file =
        use sr = new 'Reader(file)
        while not sr.EndOfStream do
            printfn "%s" <| sr.ReadLine()

and simply define FakeReader to have a constructor that takes the file name, the EndOfStream property getter, the ReadLine() method, and the (empty) Dispose() method. However, F# has several complaints about this type definition, including "Calls to object constructors on type parameters cannot be given arguments." Since StreamReader has no default constructor, this approach seems like a no-go.

So far the only way I've gotten this to work is to inherit FakeReader from StreamReader:

type FakeReader() =
    inherit StreamReader("") with
    override this.ReadLine() = "go away"
    member this.EndOfStream = false
    member this.Dispose() = ()

and use a factory method that returns either a new FakeReader or a new StreamReader as appropriate.

type ReaderType = Fake | SR
let readerFactory (file : string, readerType) =
    match readerType with
    | Fake -> new FakeReader() :> StreamReader
    | SR -> new StreamReader(file)

type FileReader(readertype) =
    member this.Read file =
        use sr = readerFactory(file, readertype)
        while not sr.EndOfStream do
            printfn "%s" <| sr.ReadLine()

This seems a lot less elegant. Is there a way to do this with a type parameter? Thanks to all.

Upvotes: 2

Views: 1378

Answers (2)

Markus Jarderot
Markus Jarderot

Reputation: 89241

You could pass in a function that constructs and returns an object of your desired type.

type FileReader(f : string -> TextReader) =
    member this.Read file =
        use sr = f file
        while sr.Peek() <> -1 do
            printfn "%s" <| sr.ReadLine()

type FakeReader() =
    inherit StringReader("")
    override this.ReadLine() = "go away"
    override this.Peek() = 0

let reader1 = new FileReader(fun fn -> new StreamReader(fn) :> _)

let reader2 = new FileReader(fun fn -> new FakeReader() :> _)

Cast was necessary because I dropped the generic type-argument, but the actual type can be inferred.

Upvotes: 3

Tomas Petricek
Tomas Petricek

Reputation: 243096

Using a function that creates a reader object (as suggested by MizardX) is the direct answer to your question. However, I'd maybe consider using a different abstraction than TextReader). As Ankur mentioned in a comment, you could use a more functional approach.

If you're just reading lines of text from the input using TextReader, you could use a seq<string> type instead. The FileReader type may actually be just a function taking seq<string> (although that may be oversimplification... it depends).

This makes it more "functional" - in functional programming, you're often transforming data structures using functions, which is exactly what this example does:

open System.IO

/// Creates a reader that reads data from a file
let readFile (file:string) = seq {
  use rdr = new StreamReader(file)
  let line = ref ""
  while (line := rdr.ReadLine(); !line <> null) do
    yield !line }

/// Your function that processes the input (provided as a sequence)
let processInput input = 
  for s in input do 
    printfn "%s" s

readFile "input.txt" |> processInput

To test the processInput function, you could then create a new seq<string> value. This is significantly easier than implementing a new TextReader class:

let testInput = seq {
  yield "First line"
  yield "Second line"
  raise <| new System.OutOfMemoryException() }

testInput |> processInput

Upvotes: 4

Related Questions