Функциональные интерфейсы… в VBA

в 17:22, , рубрики: map reduce, vba, функциональное программирование

"…те, кто не прочь поглазеть на любителя прилюдно свалять дурака, пусть понаблюдают, как я доказываю, что 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

Источник

* - обязательные к заполнению поля