Статьи

Как работает Скала это магия?

Привет всем, сегодня мы немного поговорим об упомянутой «автоматике» Scala, о том, как она делает то, что делает, и в конце концов генерирует Java-код. Потому что, если вы подумаете об этом, если все, что он делает, это генерирует классы Java, то мы должны просто сделать это напрямую с помощью Java, верно? Неправильно. Я не собираюсь углубляться в сложные концепции языка, которые выходят за рамки этого поста, я просто покажу немного того, что он делает, и, возможно, это очистит некоторые ваши мысли от его внутренние работы. Это, безусловно, сделало это для меня в первый раз, когда я увидел что-то вроде того, что я собираюсь представить здесь. А также покажите немного того, что можно сделать с помощью коллекций Scala.

Я собираюсь показать некоторые примеры кода Java, из ситуаций, с которыми мы можем столкнуться в наш обычный день разработки, но, конечно, примеры, размещенные здесь, будут просты для обучения. Начнем?

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

1
2
3
for(String str : strings) {
    // execute function on str
}

А что если нам нужно сделать что-то подобное в другом списке?

1
2
3
for(Integer number : numbers){
    // execute function on number
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public static void main(String[] args) throws Exception{
    List<String> strings = Arrays.asList("Simple", "Java", "String", "List");
    List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5);
 
    List<String> newStrings = new ArrayList<String>();
    for (String str : strings){
        newStrings.add(str + "Verbose");
    }
 
    List<Integer> newNumbers = new ArrayList<Integer>();
    for (Integer number : numbers){
        newNumbers.add(number * number);
    }
 
    System.out.println(newStrings);
    System.out.println(newNumbers);
}

Теперь я спрашиваю вас, как мы можем избежать этого повторения? Как мы можем выразить только то, что мы хотим, в выделенных линиях?

Ну, мы могли бы поместить все это в метод, но … как мы могли бы избежать повторения, если мы не можем отправить метод в качестве аргумента другому методу?

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

Сначала мы создаем наш интерфейс с именем Transformer:

1
2
3
4
5
public interface Transformer<F, T>{
 
    public T transform(F f);
 
}

Он имеет только одну сигнатуру метода, которая получает F и возвращает T , здесь ничего нового.

А теперь мы создадим наш собственный список, который будет использовать этот интерфейс в своем методе transform .

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
public class MyList<E> extends ArrayList<E>{
 
    public MyList(){}
 
    public MyList(E... elements){
        for(E element : elements){
            add(element);
        }
    }
 
    public <T> MyList<T> transform(Transformer<E, T> transformer){
        MyList<T> myList = new MyList<T>();
        Iterator<E> ite = iterator();
        while(ite.hasNext()){
            myList.add(transformer.transform(ite.next()));
        }
        return myList;
    }
 
}

И теперь все, что нам нужно сделать, это сообщить методу transform, как преобразовать наш проект, посмотрите пример кода, переписанный с использованием нашей новой структуры.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args){
    MyList<String> strings = new MyList<String>("Simple", "Java", "String", "List");
    MyList<Integer> numbers = new MyList<Integer>(1, 2, 3, 4, 5);
 
    MyList<String> newStrings = strings.transform(new Transformer<String, String>(){
        public String transform(String str){
            return str + " Verbose";
        }
    });
 
    MyList<Integer> newNumbers = numbers.transform(new Transformer<Integer, Integer>(){
        public Integer transform(Integer num){
            return num * num;
        }
    });
 
    System.out.println(newStrings);
    System.out.println(newNumbers);
}

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

Теперь все становится проще, в Scala у нас есть функция map, которая делает именно это, она применяет данную функцию к каждому элементу списка и возвращает измененный список (хорошо помнить, что исходный список остается неизменным). Смотрите тот же пример, который сейчас написан на Scala:

01
02
03
04
05
06
07
08
09
10
object Main extends App{
    val strings = List("Awesome", "Scala", "String", "List")
    val numbers = 1 to 5
 
    val newStrings = strings.map(_ + " Clean")
    val newNumbers = numbers.map(x => x * x)
 
    println(newStrings)
    println(newNumbers)
}

Код чище, правда? Это потому, что Scala работает со своей внутренней магией и берет на себя ответственность за выполнение всего, что мы делали раньше, от разработчика.

О, я вижу разницу. Но объект ? Приложение ? Что это?

Если вы не знаете, что это такое, не беспокойтесь об этом, это не важно для этого поста, просто подумайте об этом блоке кода как о старом пустом основном Java-методе.

Хорошо, но как Scala делает все это возможным?

То, что делает Scala автоматически, это то, что мы делали в предыдущем Java-коде, в анонимном внутреннем классе он определяет функцию, которая будет применена к каждому элементу нашего списка, интересно, а? Разумеется, в процессе компиляции происходят другие изменения, но в процессе компиляции происходят адаптации и оптимизации, но в основном это то, что происходит.

Давайте посмотрим на другой пример, фильтрация списков. В этом примере единственное, что меняется, — это критерии того, что следует добавить в отфильтрованный список, остальной код — это повторение, повторение, повторение.

Опять же, мы начнем с примера Java:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args){
    List<Integer> numbers = Arrays.asList(12345678910);
 
    List<Integer> evenNumbers = new ArrayList<Integer>();
    for(Integer number : numbers){
        if(number % 2 == 0){
            evenNumbers.add(number);
        }
    }
    List<Integer> oddNumbers = new ArrayList<Integer>();
    for(Integer number : numbers){
        if(number % 2 != 0){
            oddNumbers.add(number);
        }
    }
 
    System.out.println(evenNumbers);
    System.out.println(oddNumbers);
}

Теперь у нас будет новый интерфейс, чтобы помочь нам с методом фильтрации, изначально названным Filter .

1
2
3
4
5
public interface Filter<E>{
 
    public boolean matchesRequirement(E e);
 
}

И как у нас уже есть наш суперспециальный список, все, что нам нужно сделать, это добавить в него этот новый метод.

01
02
03
04
05
06
07
08
09
10
11
public MyList<E> filter(Filter<E> filter){
    MyList<E> myList = new MyList<E>();
    Iterator<E> ite = iterator();
    while(ite.hasNext()){
        E element = ite.next();
        if(filter.matchesRequirement(element)){
            myList.add(element);
        }
    }
    return myList;
}

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args){
    MyList<Integer> numbers = new MyList<Integer>(12345678910);
 
    MyList<Integer> evenNumbers = numbers.filter(new Filter<Integer>(){
        public boolean matchesRequirement(Integer number){
            return number % 2 == 0;
        }
    });
 
    MyList<Integer> oddNumbers = numbers.filter(new Filter<Integer>(){
        public boolean matchesRequirement(Integer number){
            return number % 2 != 0;
        }
    });
 
    System.out.println(evenNumbers);
    System.out.println(oddNumbers);
 
}

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

1
2
3
4
5
6
7
8
9
object Main extends App{
    val numbers = 1 to 10
 
    val evenNumbers = numbers.filter(_ % 2 == 0)
    val oddNumbers = numbers.filter(_ % 2 != 0)
 
    println(evenNumbers)
    println(oddNumbers)
}

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

Так что насчет этих интерфейсов? Создает ли Scala новую функцию для каждой функции, которую я хочу реализовать?

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

Если вы хотите посмотреть официальную документацию, вы можете проверить Function , Function1 , Function2 и PartialFunction .

А что если я захочу преобразовать все элементы в моем списке номеров, умножив их на себя, и в этом новом списке отфильтровать все элементы, которые делятся на 5?

А? Зачем кому-то это делать?

Хорошо, это дурацкий пример, но он просто для сравнения фрагментов кода, посмотреть, как он будет выглядеть в Java с нашим новым классным списком:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
public static void main(String[] args){
 
    MyList<Integer> numbers = new MyList<Integer>(12345678910);
 
    MyList<Integer> newNumbers = numbers.transform(new Transformer<Integer, Integer>(){
        public Integer transform(Integer n){
            return n * n;
        }
    }).filter(new Filter<Integer>(){
        public boolean matchesRequirement(Integer number){
            return number % 5 == 0;
        }
    });
 
    System.out.println(newNumbers);
 
}

Мы можем легко увидеть, что чем больше мы хотим исследовать это с Java, тем более многословным становится наш код, поэтому я действительно никому не рекомендую делать такие вещи. С другой стороны, это добавит ненужных сложностей …

1
2
3
4
5
6
7
object Main extends App {
    val numbers = 1 to 10
 
    val newNumbers = numbers.map(n => n * n).filter(_ % 5 == 0)
 
    println(newNumbers)
}

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

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