Рубрики
Без рубрики

Идите за разработчиками Java

голанг и яванские ламбасы. Помечено как go, java, программирование, веселье.

В последний день мне пришлось выполнить задачу в go.

Как вы, возможно, уже знаете из этого поста, я недавно начал программировать на Go, поэтому довольно часто первый вопрос, который возникает у меня в голове: как бы я это сделал на Java?

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

Проблема

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

Файл выглядит следующим образом:

// a comment here 
#HEADER 
Key1=value1 
//another comment 
Key2=value2 
Key3=value3 
Key4=value4 

Нас интересуют два ключа, которые принадлежат разделу. Раздел начинается с заголовка, который начинается с хэштега. Нам дается название раздела и название двух ключей. т.е. раздел: “лицо”, ключи: [“имя”, “фамилия”] Поэтому мы хотим найти раздел “личность” в файле и загрузить значения имени и фамилии в память.

Пара ошибок по поводу файла:

  • Заголовок может отсутствовать в файле
  • имя и/или фамилия могут отсутствовать в файле
  • имя всегда предшествует фамилии в файле
  • Он может содержать комментарии (//) или другие маркеры

Одно из возможных решений – просто прочитать файл построчно и:

  • если строка начинается с #, то это начало раздела, приготовьтесь прочитать пару ключей для этого заголовка
  • Прочитайте имя и фамилию
  • Игнорировать комментарии и остальные ключи

Итак, давайте на секунду наденем шляпу Java-программиста.

Итак, чтение файла, строка за строкой, фильтрация некоторых строк…

Хорошо, это похоже на работу для java.nio.файла. Файлы , a java.util.поток. Поток и пользовательская java.util.функция. Предикат для фильтрации потока. Я говорю обычай, имея в виду, что нам нужно сохранить некоторое состояние в нашем предикате. Состояние – это просто строка, которой должен соответствовать предикат при следующем вызове, т.Е. Если он уже соответствует заголовку, то затем должен соответствовать имени.

В основном это означает две вещи: потоковые и лямбда-функции.

Давайте короче, в Голанге нет такой вещи, как ручей. Позвольте мне здесь немного отвлечься, весь смысл этого умственного упражнения не в том, чтобы реализовать что-то, что противоречит самому языку, а в том, чтобы извлечь из этого максимум пользы. Так что никакого ручья. Мы собираемся жить без этого, ибо петли приходят на помощь.

Но как насчет лямбд? Что ж, функции в Go – это граждане первого класса. Мы можем назначать их переменным, создавать массив функций, передавать их, как и любой другой аргумент, так что, я думаю, у нас есть наши лямбды:)

Ладно, хватит разговоров, давайте посмотрим код… Подождите, прежде чем смотреть на код, давайте резюмируем, что мы хотим сделать. Таким образом, мы ожидаем, что у нас будет цикл for, в котором мы читаем содержимое из источника построчно и для каждой строки проверяем, должно ли оно быть где-то написано. Бит, который проверяет, должна ли строка быть записана, является фильтром, который внутренне сохраняет состояние.

Хорошо, давайте посмотрим на этот кусочек:

func copy(profileName string, in io.Reader, out io.Writer) error {
    var (
        line       string
        readError  error
        writeError error
    )

    profileFilter := newProfileFilter(profileName)
    reader := bufio.NewReader(in)

    for {
        line, readError = reader.ReadString('\n')

        if profileFilter.match(line) {
            _, writeError = io.WriteString(out, line)
        }

        if readError != nil || writeError != nil {
            break
        }
    }

     if writeError != nil {
        return writeError
     }

     if readError != io.EOF {
        return readError
     }

     return nil
}

Это в значительной степени “эквивалентно” Java

Files.lines(resource())
             .filter(profileFilter)
             .forEach(ProfileWriter::write); 

Давайте немного разберем это:

  • Файлы.строки – это цикл для
  • Фильтр – это условие if фильтр профиля.совпадение(строка)
  • forEach – это просто тело условия if ввода-вывода. Строка записи(выход, строка)

Итак, давайте посмотрим на фильтр:

func newProfileFilter(profileName string) *profileFilter {
    var matchers [](func(line string) bool)

    matchers = append(matchers,
        matcher(startsWith).apply("#"+profileName),
        matcher(startsWith).apply(),
        matcher(startsWith).apply(),
    )

    return &profileFilter{matchers, profileName}
}

func startsWith(line string, toMatch string) bool {
    return strings.HasPrefix(
        strings.ToLower(strings.TrimSpace(line)),
        strings.ToLower(toMatch),
    )
}

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

matcher(startsWith).apply()

Итак, мы создаем сопоставитель, который соответствует строке, начинающейся с “имя =”, Что на самом деле соответствует?

type matcher func(line string, toMatch string) bool

Это псевдоним типа для функции. Так что это просто функция, которая получает в качестве входных данных строку и строку для сопоставления и возвращает логическое значение. Это оно. Синтаксис сопоставитель(начинается с)

когда я это писал, я был такой…что ж, функции в go чертовски крутые

Что это за метод применения?

func (f matcher) apply(toMatch string) func(line string) bool {
    return func(line string) bool {
        return f(line, toMatch)
    }
}

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

Итак, здесь мы применяем строку, которую мы хотим сопоставить, например “имя =”, и возвращаем функцию, которая имеет в качестве параметра только строку. Мы прикрепили метод apply к совпадениям, поэтому конечным результатом является то, что при вызове:

matcher(startsWith).apply() 

Мы возвращаем функцию, которая возвращает логическое значение (предикат на языке java), которое будет истинным, если строка начинается с name.

Это остальная часть кода здесь просто для полноты картины.

type profileFilter struct {
    matchers    []func(line string) bool
    profileName string
}

func (p *profileFilter) match(text string) bool {
    if len(p.matchers) == 0 {
        return false
    }

    shouldFilter := p.matchers[0](text)

    if shouldFilter {
        p.matchers = p.matchers[1:len(p.matchers)]
    }

    return shouldFilter
}

Версия Java

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

    public static void main(String[] args) throws IOException, URISyntaxException{
        Matcher startsWith = (line, toMatch) -> startsWith(line, toMatch);

        ProfileFilter profileFilter =
            new ProfileFilter(
                startsWith.apply("#some-profile"),
                startsWith.apply(),
                startsWith.apply()
            );

        Files.lines(resource())
             .filter(profileFilter)
             .forEach(System.out::println);
    }

    static class ProfileFilter implements Predicate {
        private LinkedList
> predicates;

        ProfileFilter(Predicate...predicates) {
            this.predicates = new LinkedList<>(Arrays.asList(predicates));
        }

        @Override
        public boolean test(String s) {
            if (predicates.size() == 0) {
                return false;
            }

            boolean shouldFilter = predicates.getFirst().test(s);

            if (shouldFilter) {
                predicates.removeFirst();
            }

            return shouldFilter;
        }
    }

    interface Matcher extends BiPredicate {

        default Predicate apply(String applied) {
            return s -> this.test(s, applied);
        }
    }

    static Boolean startsWith(String line, String toMatch) {
        return line.toLowerCase().trim().startsWith(toMatch.toLowerCase());
    }

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

Примечание здесь: Потоки действительно классные, но они не ладят с исключениями. Запись в файл, например, является одной из тех операций, которые вызывают исключение, поэтому в реальном случае использования я думаю, что я бы использовал цикл для вместо потока или RxJava Наблюдаемый .

Выводы

Ладно, это оказалось длиннее, чем я ожидал, учитывая, что я хотел просто сказать, что, вероятно, Голанг не так уж плох XD. Шутки в сторону, надеюсь, вам понравилось.

Спасибо!

Ресурсы

Код для статьи: github Я нашел два интересных выступления о функциях в Golang. Полностью стоит того, чтобы посмотреть на них: Замыкания являются обобщениями go Не выполняйте функцию первого класса

Оригинал: "https://dev.to/napicella/go-for-java-developers-lambda-functions-to-the-rescue-655"