"…те, кто не прочь поглазеть на любителя прилюдно свалять дурака, пусть понаблюдают, как я доказываю, что Java и Visual Basic – близнецы, разлученные при рождении, а С++ им даже не дальний родственник."
Брюс Мак-Кинни “Крепкий орешек Visual Basic”
Введение
Постоянный интерес к подходам функционального программирования в настоящее время приводит к тому, что традиционные языки программирования активно обзаводятся функциональными средствами. И, хотя чистые функциональные языки остаются пока не слишком популярными, функциональные возможности прочно обосновались в таких языках, как С++, Java, JavaScript, Python и др. Язык VBA уже многие годы пользуется заслуженной популярностью у довольно многочисленной аудитории пользователей Microsoft Office, однако этот язык практически не содержит функциональных средств.
Давайте попытаемся заполнить этого пробел – предлагаю законченную (хотя, возможно, и не безупречную) реализацию функциональных интерфейсов, выполненную средствами VBA. Реализация может служить основой для последующих доработок и улучшений.
Проблема функциональных аргументов
Первая проблема, с которой мы столкнемся на этом пути – это проблема передачи функциональных аргументов в функцию или метод. Язык VBA не содержит соответствующих средств (оператор AddressOf служит лишь для передачи адресов функциям Windows API и не вполне безопасен в работе). Это же можно сказать и об известной методике вызова функций по указателю (Магдануров Г.И. Visual Basic на практике СпБ.: “БХВ Петербург”, 2008). Давайте не будем рисковать — используем при реализации только стандартные возможности языка и стандартные библиотеки.
К сожалению, здесь нам ООП мало чем поможет. Для передачи функционального объекта в процедуру или функцию язык VBA предлагает стандартную возможность – обернуть нужную функциональность объектной оболочкой (создать объект, одним из методов которого и будет нужная функциональность). Объект можно передать как параметр. Этот подход работоспособен, однако весьма тяжеловесен – для каждой нужной функциональности придется создавать свой класс и объект этого класса.
Существует и другой способ, который оказывается существенно проще и не требует создания отдельных классов для каждой функциональности.
Предположим, что в некую процедуру proc требуется передать анонимную функцию, которая увеличивает свой аргумент на единицу. Эту функцию можно записать так:
x -> x+1
Подобная нотация задания анонимных функций в настоящее время уже практически стала “стандартом де факто”. Единственная возможность передать такую функцию параметром состоит в использовании строкового представления:
r=proc(a,b,”x->x+1”)
здесь a и b – обычные параметры, а третий параметр – безымянная функция, что весьма наглядно и мало отличается от записей в популярных языках программирования.
Чтобы использовать анонимную функцию, заданную подобным образом, ее необходимо сначала привести к стандартному виду функции VBA. Это выполняет следующая служебная процедура:
Private Function prepCode(Code As String) As String
k% = InStr(Code, "->")
parms$ = Trim$(Left$(Code, k% - 1))
body$ = Mid$(Code, k% + 2)
If Left$(parms$, 1) <> "(" Then parms$ = "(" + parms$ + ")"
If InStr(body$, "self") = 0 Then body$ = ";self=" & body$ & ";"
body$ = Replace(body$, ";", vbCrLf)
prepCode = "function self" & parms & vbCrLf & body & _
vbCrLf & "end function"
End Function
Функция выделяет список параметров и тело вычисления, а затем формирует функцию с именем self. Для нашего случая функция self будет иметь следующий вид:
function self(x)
self=x+1
End function
Очевидно, что в соответствии с синтаксисом VBA, эта функция будет делать именно то, что должна была делать анонимная функция – увеличивает значение своего аргумента на 1. Правда, эта функция – пока не есть функция VBA, а только строка, содержащая указанный код. Для того, чтобы превратить строку в функцию, можно использовать стандартную майкрософтовскую библиотеку “Msscript.ocx”. Эта COM-библиотека позволяет выполнить произвольный код VBA, заданный в строковой форме. Для этого необходимо выполнить следующее:
— Создать объект ScriptControl
— Вызвать метод установки языка (VBScript);
— Вызвать метод загрузки функции;
— Вызвать метод eval для исполнения вызова.
Все это выглядит примерно так:
Set locEv=new ScriptControl
locEv.Language = "VBScript"
locEv.AddCode prepCode(“x->x+1”)
r=lovEv.eval(“self(5)”)
После выполнения данного кода значение переменной r будет равно 6.
Здесь следует сделать три замечания:
— Тело анонимной функции может содержать несколько строк. Отдельные операторы в этом случае завершаются точкой с запятой. Из окончательного кода символы “;” исключаются. Многострочное тело позволяет реализовывать в анонимных функциях весьма продвинутую функциональность;
— То, что анонимная функция “в действительности” имеет имя “self”, дает неожиданный бонус – анонимная функция может быть рекурсивной.
— Поскольку объект ScriptControl поддерживает два языка – VBScript и Jscript, то безымянная функция может быть (теоретически) написана и на Jscript (но в настоящей работе эта возможность не реализована).
Далее будет описана объектная модель реализации.
Объектная модель
Основой модели являются объекты двух видов: Container и Generator. Объект Container является хранилищем массива произвольных размеров, объект Generator, как следует из названия, реализует генератор общего вида.
Оба объекта реализуют интерфейс aIter, который более подробно описывается ниже. Интерфейс включает 19 функций:
Имя метода | Параметры | Результат |
isGen | - | Возвращает True, если объект является генератором |
isCont | - | Возвращает True, если объект является контейнером |
getCont | - | Для контейнера возвращает локальный массив, для генератора возвращает Empty |
getNext | - | Возвращает следующее значение |
hasNext | - | Возвращает True, если следующее значение имеется |
Init | iniVal As Variant, lambda As String = "", emptyC As Boolean = False | iniVal – начальное значение; lambda – анонимная функция для генератора emptyC – при задании True создается пустой контейнер |
Take | n as integer | Возвращает контейнер, содержащий n последовательных значений, полученных из исходного объекта |
Filter | lambda as string | Возвращает объект, полученный фильтрацией исходного в соответствии с безымянной функцией lambda |
Map | lambda as string | Возвращает объект, полученный мапированием исходного в соответствии с безымянной функцией lambda |
Reduce | acc As Variant, lambda As String, | Возвращает результат свертки текущего объекта с начальным значением аккумулятора acc и сворачивающей функцией, заданной параметром lambda |
takeWhile | n As Integer, lambda As String |
Возвращает контейнер, содержащий n (или менее) последовательных значений, удовлетворяющих предикату, заданному безымянной функцией lambda |
dropWhile | n As Integer, lambda As String |
Возвращает контейнер, содержащий n (или менее) последовательных значений, полученных из исходного после пропуска значений, удовлетворяющих предикату, заданному функцией lambda. |
zip | iter As aIter, n As Integer = 10 |
Принимает контейнер или генератор, а возвращает контейнер, содержащий пары значений – из базового контейнера и из контейнера-параметра. Размер результата по умолчанию – десять. |
zipWith | iter As aIter, lambda As String, n As Integer = 10 |
Принимает контейнер и безымянную функцию двух аргументов. Возвращает контейнер, содержащий результаты применения заданной функции к последовательным парам – одно значение из базового контейнера, другое – из контейнера-параметра. |
Size | — | Для контейнера возвращает количество элементов |
summa | — | Сумма значений контейнера |
production | — | Произведение значений контейнера |
maximum | — | Максимальное значение в контейнере |
minimum | — | Минимальное значение в контейнере |
Для объекта-генератора ряд методов впрямую не реализован – необходимо сначала отобрать некоторое количество значений в контейнер. При попытке вызвать для генератора нереализованный метод, генерируется ошибка с кодом 666. Далее будет рассмотрено несколько примеров использования описанных интерфейсов.
Примеры
Печать последовательных чисел Фибоначчи:
Sub Test_1()
Dim fibGen As aIter
Set fibGen = New Generator
fibGen.Init Array(1, 0), "(c,p)->c+p"
For i% = 1 To 50
Debug.Print fibGen.getNext()
Next i%
End Sub
Здесь создается генератор с начальными значениями 0 и 1 и генерирующей функцией, соответствующей последовательности Фибоначчи. Далее в цикле печатаются первые 50 чисел.
Мапирование и фильтрация:
Sub Test_2()
Dim co As aIter
Dim Z As aIter
Dim w As aIter
Set co = New Container
co.Init frange(1, 100)
Set Z = co.map("x -> 1.0/x"). _
take(20).filter(" x -> (x>0.3) or (x<=0.1)")
iii% = 1
Do While Z.hasNext()
Debug.Print iii%; " "; Z.getNext()
iii% = iii% + 1
Loop
End Sub
Создается контейнер и инициализируется числовой последовательностью из диапазона от 1 до 100. Далее числа с помощью map заменяются на обратные. Из них берется двадцать первых. Далее эта совокупность фильтруется и из нее отбираются числа, большие 0.3 или меньшие 0.1. Результат возвращается в контейнере, состав которого распечатывается.
Использование свертки:
Sub Test_4()
Dim co As aIter
Set co = New Container
co.Init frange(1, 100)
v = co.reduce(0, "(acc,x)->acc+x")
Debug.Print v
v = co.reduce(1, "(acc,x)->acc*x")
Debug.Print v
End Sub
Здесь с помощью свертки считается сумма и произведение чисел от 1 до 100.
Sub Test_5()
Dim co1 As aIter
Dim co2 As aIter
Dim co3 As aIter
Set co1 = New Generator
co1.Init Array(123456789), "x -> INT(x/10)"
Set co2 = co1.takeWhile(100, "x -> x > 0")
Set co3 = co2.map("x -> x mod 10")
Debug.Print co3.maximun
Debug.Print co3.minimum
Debug.Print co3.summa
Debug.Print co3.production
End Sub
В этом примере строится генератор co1, последовательно делящий исходное число на степени 10. Затем отбираются частные до появления нуля. После чего полученный список частных отображается функцией взятия остатка от деления на 10. В результате получается список разрядов числа. Список суммируется, у него вычисляется максимум, минимум и произведение.
Выводы
Предлагаемый оказывается вполне работоспособным и может быть с успехом применен для решения повседневных задач VBA-программиста в функциональном стиле. Чем мы хуже джавистов?
Скачать примеры можно здесь
Удачи!!!
Автор: catstail1954