Как написать функциональный файл "сканер"

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

Я хотел получить некоторые предложения о том, как я могу справиться с проблемой, которую я имею функциональным образом, особенно в 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))))))))))

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

Обновлено -- вот любое обновленное решение, основанное на обратной связи I получено:

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

  • код использует много комбинаторов и функциональных композиций в шаблонах, которые не слишком распространены в 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 }