Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to add line numbers to a text file in functional programming (F#)?

Tags:

readlines

f#

seq

It works with a for loop and mutable variable:

let addLnNum filename =    
    use outFile = new StreamWriter(@"out.txt")    
    let mutable count = 1
    for line in File.ReadLines(filename) do
        let newLine = addPre (count.ToString()) line
        outFile.WriteLine newLine
        count <- count + 1

But it is very "non-functional" so I'm curious what is the proper way to do this? I figured how to append the index number to a list of strings:

let rec addIndex (startInd:int) l=
    match l with
    |x::xs ->  startInd.ToString()+x :: (addIndex (startInd+1) xs)
    |[] -> []

But it won't apply to File.ReadLines:

let addLnNum2 filename =    
    use outFile = new StreamWriter(@"out.txt")    
    File.ReadLines(filename)
    |> addIndex 1
    |> ignore
    //Error 1   Type mismatch. Expecting a Collections.Generic.IEnumerable<string> -> 'a    
    //but given a string list -> string list    

Is reading the whole file into memory as a list the only way to do this? Is there something like seq.count so it can be done similar to the following?

let addLnNum3 filename =    
    use outFile = new StreamWriter(@"out.txt")    
    File.ReadLines(filename)
    |> Seq.map (fun s -> Seq.count + s) //no such thing as Seq.count
    |> Seq.iter outFile.WriteLine 
    |> ignore
like image 864
jack3694078 Avatar asked Sep 22 '15 08:09

jack3694078


2 Answers

For some functions in the Seq module (same with List, ...) you'll find versions with an appended i - for example for Seq.map you'll find Seq.mapi and this is what you are looking for - in addition to the value from your collection you get (as the first parameter) the index too:

let addLnNums filename =    
    use outFile = new System.IO.StreamWriter (@"out.txt")
    System.IO.File.ReadLines filename
    |> Seq.mapi (sprintf "%d: %s")
    |> Seq.iter outFile.WriteLine

also note that you don't need ignore as Seq.iter already returns () : unit

If we would not have this then the functional way would be to use Zip like this:

let addLnNum filename =    
    use outFile = new System.IO.StreamWriter (@"out.txt")
    Seq.zip (Seq.initInfinite id) (System.IO.File.ReadLines filename)
    |> Seq.map (fun (index, line) -> sprintf "%d: %s" index line)
    |> Seq.iter outFile.WriteLine

which (aside from uncurrying the function to map) does basically the same


Note:

For lists you obviously don't have a List.initInfinte, so just go with Seq - also Seq.zip and List.zip have different behaviors concerning collections with different item-count - Seq.zip stops when one collection runs try but List.zip wants both lists to be of the same size and will throw an exception if not

like image 121
Random Dev Avatar answered Oct 01 '22 02:10

Random Dev


Your addIndex function is actually correct - but it works on F# lists. The ReadLine function returns IEnumerable<T> rather than an F# list (which makes sense, because it is a .NET library). You can fix the addLnNum2 function by adding List.ofSeq (to convert IEnumerable<T> to a list):

let addLnNum2 filename =    
    let added = 
      File.ReadLines(filename)
      |> List.ofSeq
      |> addIndex 1
    File.WriteAllLines("out.txt", added)

Using Seq.mapi or Seq.zip as mentioned in Carsten's answer is certainly simpler than implementing your own recursive function, but you did get the recursion and pattern matching right :-).

like image 23
Tomas Petricek Avatar answered Oct 01 '22 03:10

Tomas Petricek