Написание хорошего Scala без сопрограмм (включая пример Python с использованием yield)

В настоящее время я изучаю Scala и ищу элегантное решение проблемы, которая легко решается с помощью сопрограмм.

Поскольку сопрограммы не включены по умолчанию в Scala, я предполагаю, что они, по крайней мере, не являются общепринятой передовой практикой, и поэтому хочу написать свой код без их использования.

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

функция generate_all_matching_paths

Я хочу написать функцию, которая ищет файлы внутри базового каталога. Критерии совпадения и спуска должны быть предоставлены экземпляром класса с трейтом PathMatcher. (По крайней мере, я думаю, что это путь в Scala)

PathMatcher можно использовать, чтобы определить, соответствует ли fs_item_path, И он определяет, следует ли искать в каталоге (в случае, если fs_item_path является путем к каталогу).

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

Я хочу написать этот код в стиле Scala.

Я стремлюсь к решению с этими характеристиками:

  • быть читаемым
  • используйте идиомы Scala там, где они подходят
  • возвращать совпадающие пути во время поиска (а не после)
  • работает с очень большими коллекциями файлов (но не очень глубоко вложенными), т.е. тысячами файлов в каталоге; много миллионов файлов в общей сложности

Я предполагаю, что решение будет включать ленивую оценку потоков, но мне не удалось собрать поток в рабочем виде.

Я также читал, что при неправильном использовании ленивые потоки могут сохранять копии «старых значений». Решение, которое я ищу, не сделает этого.

аргументы

base_abs_path

абсолютный путь к каталогу, с которого начинается поиск

rel_ancestor_dir_list

список имен каталогов, которые показывают, как далеко мы спустились в подкаталоги base_abs_path

rel_path_matcher

Экземпляр класса с трейтом PathMatcher.

В приведенном ниже примере я использовал реализацию регулярного выражения, но я не хочу ограничивать использование регулярными выражениями.

Пример на Питоне

Вот полная работающая программа Python (протестированная с Python 3.4), которая включает версию Python «generate_all_matching_paths».

Программа будет искать в "d:\Projects" пути файловой системы, которые заканчиваются на "json", анализировать файлы на наличие отступов, которые они используют, а затем распечатывать результаты.

Если путь включает подстроку «python_portable», поиск не будет осуществляться в этом каталоге.

import os
import re
import codecs

#
# this is the bespoke function I want to port to Scala
#
def generate_all_matching_paths(
    base_dir_abs_path,
    rel_ancestor_dir_list,
    rel_path_matcher
):
    rooted_ancestor_dir_list = [base_dir_abs_path] + rel_ancestor_dir_list
    current_dir_abs_path = os.path.join(*rooted_ancestor_dir_list)

    dir_listing = os.listdir(current_dir_abs_path)
    for fs_item_name in dir_listing:
        fs_item_abs_path = os.path.join(
            current_dir_abs_path,
            fs_item_name
        )
        fs_item_rel_ancestor_list = rel_ancestor_dir_list + [fs_item_name]
        fs_item_rel_path = os.path.join(
            *fs_item_rel_ancestor_list
        )

        result = rel_path_matcher.match(fs_item_rel_path)
        if result.is_match:
            yield fs_item_abs_path
        if result.do_descend and os.path.isdir(fs_item_abs_path):
            child_ancestor_dir_list = rel_ancestor_dir_list + [fs_item_name]
            for r in generate_all_matching_paths(
                base_dir_abs_path,
                child_ancestor_dir_list,
                rel_path_matcher
            ):
                yield r



#
# all following code is only a context giving example of how generate_all_matching_paths might be used
#

class MyMatchResult:

    def __init__(
        self,
        is_match,
        do_descend
    ):
        self.is_match = is_match
        self.do_descend = do_descend

# in Scala this should implement the PathMatcher trait
class MyMatcher:

    def __init__(
        self,
        rel_path_regex,
        abort_dir_descend_regex_list
    ):
        self.rel_path_regex = rel_path_regex
        self.abort_dir_descend_regex_list = abort_dir_descend_regex_list

    def match(self, path):

        rel_path_match = self.rel_path_regex.match(path)
        is_match = rel_path_match is not None

        do_descend = True
        for abort_dir_descend_regex in self.abort_dir_descend_regex_list:
            abort_match = abort_dir_descend_regex.match(path)
            if abort_match:
                do_descend = False
                break

        r = MyMatchResult(is_match, do_descend)
        return r

def leading_whitespace(file_path):
    b_leading_spaces = False
    b_leading_tabs = False

    with codecs.open(file_path, "r", "utf-8") as f:
        for line in f:
            for c in line:
                if c == '\t':
                    b_leading_tabs = True
                elif c == ' ':
                    b_leading_spaces = True
                else:
                    break
            if b_leading_tabs and b_leading_spaces:
                break
    return b_leading_spaces, b_leading_tabs


def print_paths(path_list):
    for path in path_list:
        print(path)


def main():
    leading_spaces_file_path_list = []
    leading_tabs_file_path_list = []
    leading_mixed_file_path_list = []
    leading_none_file_path_list = []

    base_dir_abs_path = r'd:\Projects'

    rel_path_regex = re.compile('.*json$')
    abort_dir_descend_regex_list = [
        re.compile('^.*python_portable.*$')
    ]
    rel_patch_matcher = MyMatcher(rel_path_regex, abort_dir_descend_regex_list)

    ancestor_dir_list = []
    for fs_item_path in generate_all_matching_paths(
        base_dir_abs_path,
        ancestor_dir_list,
        rel_patch_matcher
    ):
        if os.path.isfile(fs_item_path):

            b_leading_spaces, b_leading_tabs = leading_whitespace(fs_item_path)

            if b_leading_spaces and b_leading_tabs:
                leading_mixed_file_path_list.append(fs_item_path)
            elif b_leading_spaces:
                leading_spaces_file_path_list.append(fs_item_path)
            elif b_leading_tabs:
                leading_tabs_file_path_list.append(fs_item_path)
            else:
                leading_none_file_path_list.append(fs_item_path)

    print('space indentation:')
    print_paths(leading_spaces_file_path_list)

    print('tab indentation:')
    print_paths(leading_tabs_file_path_list)

    print('mixed indentation:')
    print_paths(leading_mixed_file_path_list)

    print('no indentation:')
    print_paths(leading_none_file_path_list)

    print('space: {}'.format(len(leading_spaces_file_path_list)))
    print('tab: {}'.format(len(leading_tabs_file_path_list)))
    print('mixed: {}'.format(len(leading_mixed_file_path_list)))
    print('none: {}'.format(len(leading_none_file_path_list)))

if __name__ == '__main__':
    main()

person Robert Fey    schedule 29.01.2016    source источник
comment
Это слишком много кода, чтобы попросить нас его прочитать и понять. Можно ли свести к основному, чтобы более наглядно проиллюстрировать то, что вы хотите спросить?   -  person Seth Tisue    schedule 29.01.2016
comment
Я постарался изложить суть в тексте. Единственная важная функция в коде — generate_all_matching_paths. Остальной код, если для контекста. Я добавлю некоторые комментарии и изменю порядок :)   -  person Robert Fey    schedule 01.02.2016
comment
Что-то связанное — есть доступная библиотека Scala Coroutines.   -  person axel22    schedule 18.08.2016


Ответы (2)


Вы правы в том, что обычно заменяете python yield какой-то ленивой оценкой. Вот одно доказательство концепции, в котором класс case используется для представления каталога, чтобы избежать выполнения файлового ввода-вывода для этого примера.

case class Directory(val name: String, val files: List[String], val subDirectories: List[Directory])

def descendFilter(directory: Directory): Boolean = directory.name != "tmp"
def matchFilter(path: String): Boolean = path contains "important"

def traverse(directory: Directory, path: String = ""): Stream[String] = {
  val newPath = path + directory.name + "/"
  val files = (directory.files map (newPath + _)).toStream

  val filteredSubdirs = directory.subDirectories filter descendFilter
  val recursedSubdirs = filteredSubdirs map {x => traverse(x, newPath)}
  val combinedSubdirs = recursedSubdirs.fold(Stream.Empty)(_ ++ _)

  (path + directory.name) #:: files ++ combinedSubdirs
}

val directory = Directory("", List(), List(
  Directory("var", List("pid"), List()),
  Directory("opt", List("java"), List()),
  Directory("tmp", List("lots", "of", "temp", "files"), List()),
  Directory("home", List(), List(
    Directory("karl", List("important stuff"), List())
  ))
))

traverse(directory) filter matchFilter foreach println

В основном вы можете работать с потоком, как если бы он содержал всю файловую систему, но внутренне он будет извлекать их только по мере необходимости и так же быстро отбрасывать их, если только вы не храните ссылки на них где-то еще.

person Karl Bielefeldt    schedule 29.01.2016

Вот еще один способ сделать это в Scala (снова используя Streams):

  def recursiveListFiles(f: File, matcher: (File) => Boolean): Stream[File] = {
    val filesList = f.listFiles()
    val files = (
      if (f.listFiles == null) Array[File]()
      else filesList
      ).toStream
    val (allDirs, allFiles) = files.partition(_.isDirectory)

    allFiles.filter(matcher(_)) ++ 
        allDirs.flatMap{ d =>
           recursiveListFiles(d, matcher)
        }
  }

  def main(args: Array[String]): Unit = {
    val allFiles = recursiveListFiles(
      new File("/usr/share"),
      ((f: File) => f.getName.endsWith(".png"))) foreach println
  }
person tuxdna    schedule 29.01.2016