Reputation: 943
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
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
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