Grzenio
Grzenio

Reputation: 36689

Additional constructors in F#

I am trying to create an additional constructor in F# that does some extra work (i.e. reads a basic csv file) as follows:

type Sheet () =
  let rows = new ResizeArray<ResizeArray<String>>()
  let mutable width = 0

  new(fileName) as this = 
    Sheet() 
    then
      let lines = System.IO.File.ReadLines fileName
      for line in lines do
        let cells = line.Split ','
        rows.Add(new ResizeArray<String> (cells)) //line 16
        if cells.Length > width then width <- cells.Length

but I get the following errors:

Error   1   The namespace or module 'rows' is not defined   C:\Users\ga1009\Documents\PhD\cpp\pmi\fsharp\pmi\Csv.fs 16
Error   2   The value or constructor 'width' is not defined C:\Users\ga1009\Documents\PhD\cpp\pmi\fsharp\pmi\Csv.fs 17
Error   3   The value or constructor 'width' is not defined C:\Users\ga1009\Documents\PhD\cpp\pmi\fsharp\pmi\Csv.fs 17

What am I doing wrong?

Upvotes: 18

Views: 6264

Answers (3)

user430788
user430788

Reputation: 2241

I thought id mention that F# currently supports secondary constructors.

I solved a similar problem by simply defining secondary constructors that call the primary constructor. Here is an example from my code

type ImageSFML(tex:SFTexture,rect) =
    let sprite = new SFSprite(tex,rect)
    
    new(tex) =
        ImageSFML(tex,IntRect(
            Vector2i(0,0),
            Vector2i(int(tex.Size.X),int(tex.Size.Y))))

Upvotes: -1

Tomas Petricek
Tomas Petricek

Reputation: 243096

As Daniel pointed out, the F# design is that you have one main constructor that typically takes all the arguments needed by the class and performs the initialization. Other constructors can either provide default values for the arguments or can calculate them from some other information.

In your case, I think that the best design would be to pass rows as constructor argument. Then you can add two additional constructors (one that loads a file and other that provides empty list).

This makes the code a bit simpler, because you do not have to check if the argument is supplied (as in Daniel's version). I also did a few other simplifications (i.e. calculate width functionally and use sequence comprehensions - if Sheet does not modify the data, you could also avoid using ResizeArray):

type Sheet private (rows) =  
  // The main constructor is 'private' and so users do not see it,
  // it takes columns and calculates the maximal column length
  let width = rows |> Seq.map Seq.length |> Seq.fold max 0

  // The default constructor calls the main one with empty ResizeArray
  new() = Sheet(ResizeArray<_>())

  // An alternative constructor loads data from the file
  new(fileName:string) =
    let lines = System.IO.File.ReadLines fileName 
    Sheet(ResizeArray<_> [ for line in lines -> ResizeArray<_> (line.Split ',') ])

Upvotes: 36

Daniel
Daniel

Reputation: 47914

rows and width are not in scope. You can make members to get/set them, or (my recommendation) make the constructor with the most args the primary:

type Sheet (fileName) =
  let rows = new ResizeArray<ResizeArray<string>>()
  let mutable width = 0

  do
    match fileName with 
    | null | "" -> ()
    | _ ->
      let lines = System.IO.File.ReadLines fileName
      for line in lines do
        let cells = line.Split ','
        rows.Add(new ResizeArray<string> (cells)) //line 16
        if cells.Length > width then width <- cells.Length

  new() = Sheet("")

Generally, secondary constructors are intended to be overloads of the primary constructor, so they're prevented from interacting with class internals (fields). This encourages a single path of initialization (and better design).

Upvotes: 6

Related Questions