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

2

5

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
Divestiture answered 22/9, 2015 at 8:39 Comment(0)
F
7

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

Foist answered 22/9, 2015 at 8:46 Comment(1)
Thanks!! Much appreciated the solution with Zip method since I'm trying to learn how to do things in functional styleDivestiture
H
2

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 :-).

Hetaerism answered 22/9, 2015 at 9:4 Comment(1)
Please note that List.ofSeq will create a complete lists of all the content in memory (just commenting because it was mentioned in the question)Foist

© 2022 - 2024 — McMap. All rights reserved.