2012-01-10 20 views
6

pierwsze chciałbym przeprosić za skalę tego problemu, ale jestem naprawdę staramy się myśleć funkcjonalnie i jest to jeden z trudniejszych problemów miałem pracować.Jak napisać funkcjonalny plik „skaner”

chciałem dostać jakieś sugestie, w jaki mogę obsłużyć problem mam w sposób funkcjonalny, szczególnie w F #. Piszę program, aby przejść przez listę katalogów i na podstawie listy zawierającej regex wzorów filtrować listę plików pobieranych z katalogów i stosując drugą listę wzorców regex znaleźć mecze w tekście retreived plików. Chcę, żeby to coś zwróciło nazwę pliku, indeks linii, indeks kolumny, wzór i dopasowaną wartość dla każdego fragmentu tekstu, który pasuje do danego wzoru regex. Ponadto, wyjątki muszą być rejestrowane i są tam 3 możliwymi wyjątkami scenariusze: nie można otworzyć katalog, nie może otworzyć pliku, czytanie treści z pliku nie powiodło się. Ostatnim wymaganiem jest ilość plików "zeskanowanych" na mecze, które mogą być bardzo duże, więc cała ta sprawa musi być leniwą. Nie martwię się zbytnio o "czyste" rozwiązanie funkcjonalne, ponieważ interesuje mnie "dobre" rozwiązanie, które dobrze się czyta i dobrze działa. Ostatnim wyzwaniem jest sprawienie, aby był on współdziałający z C#, ponieważ chciałbym użyć narzędzi WinForm do przyłączenia tego algorytmu do interfejsu użytkownika. Oto moja pierwsza próba i mam nadzieję, że to wyjaśni problem:

open System.Text.RegularExpressions 
open System.IO 

type Reader<'t, 'a> = 't -> 'a //=M['a], result varies 

let returnM x _ = x 

let map f m = fun t -> t |> m |> f 

let apply f m = fun t -> t |> m |> (t |> f) 

let bind f m = fun t -> t |> (t |> m |> f) 

let Scanner dirs = 
    returnM dirs 
    |> apply (fun dirExHandler -> 
     Seq.collect (fun directory -> 
      try 
       Directory.GetFiles(directory, "*", SearchOption.AllDirectories) 
      with | e -> 
       dirExHandler e directory 
       Array.empty)) 
    |> map (fun filenames -> 
     returnM filenames 
     |> apply (fun (filenamepatterns, lineExHandler, fileExHandler) -> 
      Seq.filter (fun filename -> 
       filenamepatterns |> Seq.exists (fun pattern -> 
        let regex = new Regex(pattern) 
        regex.IsMatch(filename))) 
      >> Seq.map (fun filename -> 
        let fileinfo = new FileInfo(filename) 
        try 
         use reader = fileinfo.OpenText() 
         Seq.unfold (fun ((reader : StreamReader), index) -> 
          if not reader.EndOfStream then 
           try 
            let line = reader.ReadLine() 
            Some((line, index), (reader, index + 1)) 
           with | e -> 
            lineExHandler e filename index 
            None 
          else 
           None) (reader, 0)   
         |> (fun lines -> (filename, lines)) 
        with | e -> 
         fileExHandler e filename 
         (filename, Seq.empty)) 
      >> (fun files -> 
       returnM files 
       |> apply (fun contentpatterns -> 
        Seq.collect (fun file -> 
         let filename, lines = file 
         lines |> 
          Seq.collect (fun line -> 
           let content, index = line 
           contentpatterns 
           |> Seq.collect (fun pattern ->  
            let regex = new Regex(pattern) 
            regex.Matches(content) 
            |> (Seq.cast<Match> 
            >> Seq.map (fun contentmatch -> 
             (filename, 
              index, 
              contentmatch.Index, 
              pattern, 
              contentmatch.Value)))))))))) 

Dzięki za wszelkie dane wejściowe.

Zaktualizowany - tutaj jest zaktualizowany rozwiązanie w oparciu o opinie, które otrzymałem:

open System.Text.RegularExpressions 
open System.IO 

type ScannerConfiguration = { 
    FileNamePatterns : seq<string> 
    ContentPatterns : seq<string> 
    FileExceptionHandler : exn -> string -> unit 
    LineExceptionHandler : exn -> string -> int -> unit 
    DirectoryExceptionHandler : exn -> string -> unit } 

let scanner specifiedDirectories (configuration : ScannerConfiguration) = seq { 
    let ToCachedRegexList = Seq.map (fun pattern -> new Regex(pattern)) >> Seq.cache 

    let contentRegexes = configuration.ContentPatterns |> ToCachedRegexList 

    let filenameRegexes = configuration.FileNamePatterns |> ToCachedRegexList 

    let getLines exHandler reader = 
     Seq.unfold (fun ((reader : StreamReader), index) -> 
      if not reader.EndOfStream then 
       try 
        let line = reader.ReadLine() 
        Some((line, index), (reader, index + 1)) 
       with | e -> exHandler e index; None 
      else 
       None) (reader, 0) 

    for specifiedDirectory in specifiedDirectories do 
     let files = 
      try Directory.GetFiles(specifiedDirectory, "*", SearchOption.AllDirectories) 
      with e -> configuration.DirectoryExceptionHandler e specifiedDirectory; [||] 
     for file in files do 
      if filenameRegexes |> Seq.exists (fun (regex : Regex) -> regex.IsMatch(file)) then 
       let lines = 
        let fileinfo = new FileInfo(file) 
        try 
         use reader = fileinfo.OpenText() 
         reader |> getLines (fun e index -> configuration.LineExceptionHandler e file index) 
        with | e -> configuration.FileExceptionHandler e file; Seq.empty 
       for line in lines do 
        let content, index = line 
        for contentregex in contentRegexes do 
         for mmatch in content |> contentregex.Matches do 
          yield (file, index, mmatch.Index, contentregex.ToString(), mmatch.Value) } 

Ponownie, każdy wkład jest mile widziane.

+2

Czy widziałeś funkcjonalne parsery takie jak Parsec? –

+1

To dużo tekstu. Spróbuj go zepsuć, aby był łatwiejszy do odczytania. – Marcin

+0

Po prostu użyłbym interfejsu i Object Expression do stworzenia instancji i udostępnienia jej do kodu C#. –

Odpowiedz

8

Myślę, że najlepszym rozwiązaniem jest zacząć od najprostszego rozwiązania, a następnie przedłużyć go. Twoje obecne podejście wydaje się być dość trudne do odczytania dla mnie z dwóch powodów:

  • Kod zużywa dużo kombinatorów i kompozycji funkcyjnych wzorców, które nie są zbyt powszechne w F #. Niektóre przetwarzanie można łatwiej zapisać przy użyciu wyrażeń sekwencji.

  • Kod jest napisane w jednej funkcji, ale jest raczej skomplikowane i będzie bardziej czytelny, gdy było podzielone na wiele funkcji.

I pewnie zacząć od podziału kodu w funkcji, która testuje jeden plik (powiedzmy fileMatches) oraz funkcja, która podchodzi plików i wzywa fileMatches. Głównym iteracji może być całkiem ładnie napisane przy użyciu wyrażeń F # kolejności:

// Checks whether a file name matches a filename pattern 
// and a content matches a content pattern 
let fileMatches fileNamePatterns contentPatterns 
       (fileExHandler, lineExHandler) file = 
    // TODO: This can be imlemented using 
    // File.ReadLines which returns a sequence 


// Iterates over all the files and calls 'fileMatches' 
let scanner specifiedDirectories fileNamePatterns contentPatterns 
      (dirExHandler, fileExHandler, lineExHandler) = seq { 
    // Iterate over all the specified directories 
    for specifiedDir in specifiedDirectories do 
    // Find all files in the directories (and handle exceptions)  
    let files = 
     try Directory.GetFiles(specifiedDir, "*", SearchOption.AllDirectories) 
     with e -> dirExHandler e specifiedDir; [||] 
    // Iterate over all files and report those that match 
    for file in files do 
     if fileMatches fileNamePatterns contentPatterns 
        (fileExHandler, lineExHandler) file then 
     // Matches! Return this file as part of the result. 
     yield file } 

Funkcja jest nadal dość skomplikowane, bo trzeba przejść wiele parametrów dookoła. Owijania parametry typu prostego lub rekordu może być dobrym pomysłem:

type ScannerArguments = 
    { FileNamePatterns:string 
    ContentPatterns:string 
    FileExceptionHandler:exn -> string -> unit 
    LineExceptionHandler:exn -> string -> unit 
    DirectoryExceptionHandler:exn -> string -> unit } 

Następnie można określić zarówno fileMatches i scanner jako funkcje, które mają tylko dwa parametry, które uczynią swój kod o wiele bardziej czytelny. Coś jak:

// Iterates over all the files and calls 'fileMatches' 
let scanner specifiedDirectories (args:ScannerArguments) = seq { 
    for specifiedDir in specifiedDirectories do 
    let files = 
     try Directory.GetFiles(specifiedDir, "*", SearchOption.AllDirectories) 
     with e -> args.DirectoryEceptionHandler e specifiedDir; [||] 
    for file in files do 
     // No need to propagate all arguments explicitly to other functions 
     if fileMatches args file then yield file }