2012-01-10 8 views
6

Primero permítanme pedir disculpas por la magnitud de este problema, pero realmente estoy tratando de pensar funcionalmente y este es uno de los problemas más desafiantes con los que he tenido que trabajar.Cómo escribir un archivo funcional "escáner"

Quería obtener algunas sugerencias sobre cómo podría manejar un problema que tengo de manera funcional, particularmente en F #. Estoy escribiendo un programa para ir a través de una lista de directorios y usar una lista de patrones regex para filtrar la lista de archivos recuperados de los directorios y usar una segunda lista de patrones regex para encontrar coincidencias en el texto de los archivos recuperados. Quiero que esto devuelva el nombre de archivo, el índice de línea, el índice de columna, el patrón y el valor coincidente para cada fragmento de texto que coincida con un patrón de expresión regular determinado. Además, las excepciones deben registrarse y existen 3 posibles escenarios de excepciones: no se puede abrir el directorio, no se puede abrir el archivo, no se puede leer el contenido del archivo. El requisito final de esto es el volumen de archivos "escaneados" para los partidos podría ser muy grande, por lo que todo esto tiene que ser flojo. No estoy demasiado preocupado por una solución funcional "pura" tanto como me interesa una "buena" solución que lea bien y funcione bien. Un desafío final es hacer interoperabilidad con C# porque me gustaría utilizar las herramientas de winform para adjuntar este algoritmo a una interfaz de usuario. Aquí está mi primer intento y con suerte esto aclarará el problema:

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

Gracias por cualquier aportación.

Actualizado - aquí es cualquier solución actualizada en base a la retroalimentación que he recibido:

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) } 

Una vez más, cualquier entrada es bienvenido.

+2

¿Has visto analizadores sintácticos como Parsec? –

+1

Esto es mucho texto. Intenta dividirlo para que sea más fácil de leer. – Marcin

+0

Simplemente usaría una interfaz y Object Expression para crear una instancia y exponerla al código C#. –

Respuesta

8

Creo que el mejor enfoque es comenzar con la solución más simple y luego ampliarla. Su enfoque actual parece ser bastante difícil de leer para mí por dos razones:

  • El código utiliza una gran cantidad de combinadores y composiciones de función en los patrones que no son demasiado comunes en F #. Parte del procesamiento se puede escribir más fácilmente usando expresiones de secuencia.

  • El código está escrito como una función única, pero es bastante complejo y sería más legible si estuviera separado en múltiples funciones.

que probablemente comenzar dividiendo el código en una función que pone a prueba un solo archivo (por ejemplo fileMatches) y una función que camina sobre los archivos y llama fileMatches. La iteración principal puede ser muy bien escrito usando expresiones F # de secuencia:

// 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 } 

La función sigue siendo bastante complicado, porque hay que pasar una gran cantidad de parámetros de alrededor. Envolver los parámetros en un tipo simple o un registro podría ser una buena idea:

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

Entonces se puede definir tanto fileMatches y scanner como funciones que toman sólo dos parámetros, lo que hará que su código mucho más fácil de leer. Algo como:

// 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 } 
Cuestiones relacionadas