Как написать функциональный файл "сканер" - PullRequest
6 голосов
/ 10 января 2012

Сначала позвольте мне извиниться за масштаб этой проблемы, но я действительно пытаюсь мыслить функционально, и это одна из самых сложных проблем, с которыми мне приходилось работать.

Я хотел получить некоторые предложенияо том, как я мог бы справиться с проблемой, которая у меня есть, функционально, особенно в F #.Я пишу программу для просмотра списка каталогов и использую список шаблонов регулярных выражений для фильтрации списка файлов, извлеченных из каталогов, и использую второй список шаблонов регулярных выражений для поиска совпадений в тексте полученных файлов.Я хочу, чтобы эта вещь возвращала имя файла, индекс строки, индекс столбца, шаблон и соответствующее значение для каждого фрагмента текста, который соответствует заданному шаблону регулярных выражений.Кроме того, исключения должны быть записаны, и есть 3 возможных сценария исключений: не удается открыть каталог, не удается открыть файл, не удалось прочитать содержимое из файла.Последним требованием является то, что объем файлов, «отсканированных» на совпадения, может быть очень большим, поэтому все это должно быть ленивым.Я не слишком беспокоюсь о «чистом» функциональном решении, так как меня интересует «хорошее» решение, которое хорошо читается и работает хорошо.Последняя проблема - заставить его взаимодействовать с C #, потому что я хотел бы использовать инструменты winform, чтобы прикрепить этот алгоритм к интерфейсу пользователя.Вот моя первая попытка, и, надеюсь, это прояснит проблему:

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

Спасибо за любой вклад.

Обновлено - вот любое обновленное решение, основанное на полученных мною отзывах:

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

Опять же, любой вход приветствуется.

1 Ответ

8 голосов
/ 10 января 2012

Я думаю, что лучший подход - начать с самого простого решения, а затем расширить его.Ваш нынешний подход кажется мне довольно сложным для чтения по двум причинам:

  • В коде используется множество комбинаторов и композиций функций в шаблонах, которые не слишком распространены в F #.Некоторая обработка может быть легче написана с использованием выражений последовательности.

  • Код написан как одна функция, но он довольно сложный и будет более читабельным, если он будет разделен нанесколько функций.

Возможно, я бы начал с разделения кода на функцию, которая проверяет один файл (скажем, fileMatches), и функцию, которая просматривает файлы и вызывает fileMatches,Основная итерация может быть довольно хорошо написана с использованием выражений последовательности F #:

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

Функция все еще довольно сложна, потому что вам нужно передать много параметров.Хорошей идеей может быть обтекание параметров простым типом или записью:

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

Затем вы можете определить fileMatches и scanner как функции, которые принимают только два параметра, что сделает ваш коднамного более читабельным.Что-то вроде:

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