I'm fairly new to F# and have written a program to parse PLY files - this is however done in an imperative way with mutable values and as far as I know that should be avoided in functional languages.
The program works, but is the performance weakened by this imperative way of doing things? How is the coding style?
module PLYparsing
open System.IO;;
open System.Collections.Generic;;
open System.Text.RegularExpressions;;
// The types in a PLY file (at least in the ones we use)
type Vertice = V of float * float * float;;
type Face = F of int * int * int;;
/// <summary>Read all lines in a file into a sequence of strings.</summary>
/// <param name="fileName">Name of file to be parsed - must be situated in Resources folder.</param>
/// <returns>A sequence of the lines in the file.</returns>
let readLines (fileName:string) =
let baseName = Directory.GetParent(__SOURCE_DIRECTORY__).FullName
let fullPath = Path.Combine(baseName, ("Resources\\" + fileName))
seq { use sr = new StreamReader (fullPath)
while not sr.EndOfStream do
yield sr.ReadLine() };;
// Mutable values to be assigned during parsing.
let mutable vertexCount = 0;;
let mutable faceCount = 0;;
let mutable faceProperties = ("", "");;
let mutable (vertexProperties: (string * string) list) = [];;
let mutable (objectInfo: (string * string) list) = [];;
let mutable (vertices: seq<Vertice>) = Seq.empty;;
let mutable (faces: seq<Face>) = Seq.empty;;
// Malformed lines in the PLY file? Raise this exception.
exception ParseError of string;;
/// <summary>Checks whether a string matches a certain regex.</summary>
/// <param name="s">The string to check.</param>
/// <param name="r">The regex to match.</param>
/// <returns>Whether or not the string matches the regex.</returns>
let matchesRegex s r =
Regex.Match(s, r).Success
/// <summary>Parse the header of a PLY file into predefined, mutable values.</summary>
/// <param name="header">A sequence of the header lines in a PLY file, not including "end_header".</param>
/// <exception cref="ParseError">Raised when the input is not recognized as anything usefull.</exception>
let parseHeader (header: seq<string>) =
for line in header do
let splitted = line.Split[|' '|]
match line with
| x when matchesRegex x @"obj_info .*" ->
let a = Array.item 1 splitted
let b = Array.item 2 splitted
objectInfo <- objectInfo@[(a, b)]
| x when matchesRegex x @"element vertex \d*" ->
vertexCount <- int (Array.item 2 splitted)
| x when matchesRegex x @"property list .*" ->
let a = Array.item 2 splitted
let b = Array.item 3 splitted
faceProperties <- (a, b)
| x when matchesRegex x @"property .*" ->
let a = Array.item 1 splitted
let b = Array.item 2 splitted
vertexProperties <- vertexProperties@[(a, b)]
| x when matchesRegex x @"element face \d*" ->
faceCount <- int (Array.item 2 splitted)
| x when ((x = "ply") || matchesRegex x @"format .*") -> ()
| _ ->
System.Console.WriteLine(line)
raise (ParseError("Malformed header."));;
/// <summary>Convert a string to a vertice.</summary>
/// <param name="s">String containing a vertice.</param>
/// <returns>The converted vertice.</returns>
/// <exception cref="ParseError">Raised when the length of the input string is less that 3.</exception>
let stringToVertice (s: string) =
match s with
| s when s.Length < 3 -> System.Console.WriteLine(s)
raise (ParseError("Malformed vertice."))
| _ -> let splitted = s.Split[|' '|]
let x = Array.item 0 splitted
let y = Array.item 1 splitted
let z = Array.item 2 splitted
V(float x, float y, float z);;
/// <summary>Convert a sequence of strings to a sequence of vertices.</summary>
/// <param name="vertices">Sequence of strings to convert.</param>
/// <returns>A sequence of the converted sequences.</returns>
let parseVertices (vertices: seq<string>) =
Seq.map(fun a -> stringToVertice(a)) vertices;;
/// <summary>Convert a string to a face.</summary>
/// <param name="s">String containing a face.</param>
/// <returns>The converted face.</returns>
/// <exception cref="ParseError">Raised when the length of the input string is less that 3.</exception>
let stringToFace (s: string) =
match s with
| s when s.Length < 3 -> System.Console.WriteLine(s)
raise (ParseError("Malformed face."))
| _ -> let splitted = s.Split[|' '|]
let x = Array.item 0 splitted
let y = Array.item 1 splitted
let z = Array.item 2 splitted
F(int x, int y, int z);;
/// <summary>Convert a sequence of strings to a sequence of faces.</summary>
/// <param name="faces">Sequence of strings to convert.</param>
/// <returns>A sequence of the converted faces.</returns>
let parseFaces (faces: seq<string>) =
Seq.map(fun a -> stringToFace(a)) faces;;
/// <summary>Main function in PLY parsing. Calls all helper functions and assigns the required mutable values.</summary>
/// <param name="fileName">File to be parsed - name of file in Resources folder.</param>
let parsePLYFile fileName =
let lines = readLines fileName
// At which index is the header located? The vertices? The faces?
let bodyPos = lines |> Seq.findIndex(fun a -> a = "end_header")
let header = lines |> Seq.take bodyPos
parseHeader header
let vertexPart = lines |> Seq.skip (bodyPos + 1) |> Seq.take vertexCount
let facePart = (lines |> Seq.skip (bodyPos + vertexCount + 1) |> Seq.take faceCount)
// Parse the header, the vertices & the faces.
vertices <- parseVertices vertexPart
faces <- parseFaces facePart;;
Array.item
usages with parameterized active patterns. Just make sure the matchesRegex function stays generic enough and one-off logic doesn't start leaking in. \$\endgroup\$