Lambda на службе оконных приложений

Safoyeth 12 Ноя 2015

Сегодня я хочу поделиться одним небольшим как это сейчас принято говорить лайфхаком. Речь пойдёт об использовании lambda-функций в приложениях Qt и PyQt

Итак, как учит нас семья, школа и википедия,

Анонимная (безымянная) функция— в программировании особый вид функций, которые объявляются в месте использования и не получают уникального идентификатора для доступа к ним. Обычно при создании они либо вызываются напрямую, либо ссылка на функцию присваивается переменной, с помощью которой затем можно косвенно вызывать данную функцию.Wikipedia

В python для определения анонимных функций (которые все называют просто лямбда-функциями) используется ключевое слово lambda и синтаксис простейших lambda-функций выглядит так:

Или можно написать вот так:

или даже так:

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

Но хватит об анонимных функциях - наша цель не в том, чтобы изучить тонкости их использования, а в том, чтобы понять как использовать их в оконных приложениях.
Основным моим инструментом разработки оконных приложений является Qt и его привязка для python PyQt. Есть много причин, почему именно эту библиотеку я использую, а не другую, но об этом как-нибудь в другой раз.
Для того, чтобы понять как и где можно использовать lambda в PyQt (начну я именно с него), я написал простенькое приложение (python 3.4, PyQt5):

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

  1. Если пользователь снимает галочку с нашего чекбокса, то кнопки становятся некликабельны, если же наоборот, пользователь ставит галочку, то кнопки становятся доступными.
  2. Простая связь между спинбоксом и слайдером - изменяя значение спинбокса, автоматически изменяется значение слайдера и наоборот.
  3. Если значение спинбокса (или слайдера) становится больше 10 в текстовом поле выводится уведомление, если меньше 10, то уведомление исчезает.
  4. Клик по кнопке "Я буду увеличивать значение SpinBox" увеличит значение спинбокса (и слайдера) на 1, клик по кнопке "Я буду уменьшать значение SpinBox" соответственно уменьшает значение на 1 (естественно уведомление также будет появляться при значении больше десяти и исчезать, если значение меньше 10).
  5. Ну и наконец, кликая по кнопке "Я кнопка, вызывающая QMessageBox" будет появляться QMessageBox, причём, если в текстовом поле есть какое-то значение, то будет появляться уведомление об ошибке, если же текстовое поле пусто, то мы увидим информационное сообщение со значением спинбокса (слайдера).

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

А затем реализуем слоты:

В принципе, ничего сложного, однако некоторые наши методы (так бывает и в реальных приложениях) занимают всего-лишь одну строчку. Лично мне всегда обидно определять метод, который будет вызван только один раз и который состоит из какой-нибудь строки типа anotherWindow.show(). Вот тут то нам lambda и поможет. Давайте смело закомментируем все наши слоты и вместо них определим анонимные функции. Начнём с push2 и push3 как с самых элементарных. Достаточно переписать connect вот так:

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

Если раскомментировать методы onSpinValueChanged и onSlideValueChanged, то можно убедиться, что всё работает аналогично. Конечно, такая конструкция не блещет гармонией, более того, я практически убеждён, что это не pythonic way, но python хорош именно тем, что позволяет разрабатывать приложения крайне быстро, в режиме жёстких временных ограничений. Это во многом связано с лаконичностью языка (если вы мне не верите, то доказательство будет ниже, когда мы перейдём к C++), анонимные функции же позволяют сделать код ещё более лаконичным.
Ну и наконец, главный фокус (когда я его "открыл", помнится был очень горд собой) - анонимной функции можно передать несколько методов. Иллюстрируя сказанное,

Lambda может принять либо tuple, причём скобки обязательны, либо list. С помощью этой техники на самом деле можно создавать сколь угодно сложные анонимные слоты, но увлекаться этим сильно не советую - это не только вредит читабельности, но и может сказаться на производительности. Ну а мы заканчиваем погружение в PyQt. Последний connect также элементарен:

Я думаю, что синтаксис не требует пояснений.

В заключение этой части хочу ещё  вас предостеречь от избыточного использования этой техники. Абсолютно нормально, когда вместо того, чтобы определять целый метод и  связывать его с сигналом, вы пишете просто lambda: window.show(). Но когда ваша лямбда занимает 10 строчек с бесконечными инлайновыми выражениями, вложенными друг в друга, это не совсем то, что нужно. Будьте сдержанны! 🙂 Мы же переходим ко второй части - анонимные функции в C++.


 

Лямбда-функции в C++ появились в стандарте C++11, то есть не так давно. В сети довольно много информации по этому стандарту и по лямбдам в частности. Я в своё время пользовался этой и этой статьями от Майкрософт.

Итак, синтаксис анонимной функции выглядит так: [capture] (parameters) {body} или так [capture] (parameters)->return type {body}. Давайте по порядку, в квадратных скобках идёт так называемое предложение фиксации. Предложение фиксации - это всего-лишь список переменных во внешней области, к которым лямбда будет иметь доступ. Предложение фиксации может быть пустым, если наша лямбда ничего не захватывает. Доступ к внешним переменным может осуществляться по ссылке (&), либо по значению переменной. В круглых скобках идут параметры  - это локальные переменные. Конструкция ->return type показывает тип возвращаемого значения, к примеру для int нужно написать просто ->int.  Если лямбда ничего не возвращает, то конструкцию ->return type можно пропустить. Ну и в фигурных скобках собственно само тело лямбда-функции. Кодом проще показать, что, где и как:

Как видите, используется ключевое слово auto, для того, чтобы автоматически выводить тип возвращаемого значения. Подробнее про auto можно также прочитать у Майкрософт. Другой вариант, использовать  std::function<type>:

Этот пример аналогичен предыдущему, однако иногда он не имеет альтернатив. К примеру, рекурсивная лямбда не может быть определена через auto (в C++14 такая возможность есть):

Но в сторону теорию! Нас интересуют оконные интерфейсы, ими мы и займёмся. Для начала как и с python мы напишем простенькое приложение (небольшие различия в компоновке и названиях прошу списать на мою невнимательность и лень 🙂 ). Итак, window.h:

Вряд ли в этом файле есть что-то, что необходимо объяснить. Не менее прост и файл window.cpp:

Файл main.cpp не имеет ни одного изменения, поэтому захламлять статью не буду, оно и так всё довольно сложно читаемо... Теперь наша задача стереть/закомментировать все коннекты и все методы в файлах window.cpp и window.h и переписать все коннекты через лямбды. И как всегда, всё не так просто!
Во-первых, если мы просто напишем что-то типа

или

или

то компилятор сразу отправит нас либо в документацию, либо гуглить, кому что ближе. Погуглив (или даже почитав документацию), мы узнаем, что сигналы и слоты в Qt - это объекты (наследники QObject) и сигналы соединяются только с объектами. Лямбда объектом Qt не является, поэтому соединить её с сигналом нельзя.
Как вы понимаете, я не стал бы городить весь этот огород, если бы всё было так 🙂 На самом деле, всё прекрасно соединяется, но только в Qt5. Дело в том, что в Qt5 появился новый синтаксис соединения, почитать про все плюсы которого вы сможете на Хабре.Я же скажу только, что новый синтаксис выглядит так:

и в документации английским по белому написано:

can be used with c+11 lambda expressions

и даже приведён пример:

Тогда вперёд! Начнём с чекбокса:

Я предпочитаю всегда писать [this] вместо [=], мне кажется, что так выразительней. Смысл же этого - открыть телу лямбды доступ к нашему классу. Далее идёт объявление переменной типа int, так как мы помним, что сигнал stateChanged передаёт именно её. Ну и в теле функции обычный тернарный оператор с той лишь тонкостью (если это можно вообще назвать тонкостью), что последовательность действий должна быть заключена в скобки.
Не менее просто переписать и соединения кнопок, если push1

ещё может вызвать какие-то затруднения, то push2 и push3 совсем элементарны - и это как раз те случаи (сугубо на мой взгляд), когда применения лямбд оправдано:

Реализуем слайдер:

А теперь самое сладенькое! Написав

я получил от компилятора подарок. Любит он меня 🙂 Смысл подарка заключался в том, что он не может понять какое значение шлёт ему spin. Дело в том, что у спинбокса есть сигнал valueChanged(int i) и valueChanged(const QString &text). Я про это знал, но ожидал, что если я определяю в параметрах лямбды (int x), он меня поймёт. Оказалось, что компилятор как маленький ребёнок - всё ему нужно разжёвывать, рассказывать, объяснять. Зато как только он всё поймёт, опять же как маленький ребёнок способен делать гениальные выводы. В нашем случае, он просто скомпилирует программу 🙂
Поняв, что как-то нужно объяснить компилятору, что именно я хочу и погуглив, я нашёл ответ - и это тот самый  static_cast, та самая вишенка на торте:

Теперь всё работает!

Вот такие они, лямбды на службе оконных приложений! Пользуйтесь, и да будет ваш код работающим, компилятор послушным, а программы полезными и GNU GPL! 🙂

Если вы нашли ошибку, пожалуйста, выделите фрагмент текста и нажмите Ctrl+Enter.




Добавить комментарий

Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: