"…те, кто не прочь поглазеть на любителя прилюдно свалять дурака, пусть понаблюдают, как я доказываю, что 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=locEv.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-программиста в функциональном стиле. Чем мы хуже джавистов?

Скачать примеры можно здесь

Удачи!!!

Комментарии (13)


  1. StrangerInTheKy
    08.06.2019 22:42
    +1

    Моя программерская карьера 14 лет назад началась с того, что я два года не вылезал из экселя и VBA… Аж всплакнул от ностальгии.
    Запомню на всякий случай, вдруг случится необходимость опять с VBA столкнуться.


  1. kovserg
    08.06.2019 23:12

    VBA это конечно «шедевр». Положу в закладки рядом с проверкой массива Dim a() As String на на nil.

    If (Not a) <> (Not 0) Then
      ub=UBound(a)
    End if
    

    Но всё таки может лучше сразу написать интерпретатор нормального языка на VBA и писать на нём.


    1. kaleman
      08.06.2019 23:59

      Что за трешовый код? Проверку на инициализацию надо делать так

      Public Function IsArrayInitialized(arr) As Boolean
        On Error Resume Next
        Dim rv As Long: rv = UBound(arr)
        IsArrayInitialized = (Err.Number = 0)
      End Function 


      1. kovserg
        09.06.2019 10:36

        Незнаю что более трешовое on error resume next или (not a)<>-1


        1. kaleman
          09.06.2019 11:59
          -1

          За то я знаю. Ваш код ужасен. Если так нужно использовать проверку на инициализацию массива включайте в свои проекты функцию IsArrayInitialized(). Примеров ее реализации различными путями множество. И кстати да, как уже было сказано никакого nil — в VBA нет.


          1. Dragokas
            09.06.2019 21:09

            Я тоже считаю, что исполнение кода через возбуждение ошибки — это плохой тон. Поэтому проверяю внутреннюю структуру SAFEARRAY: AryItems()


            1. catstail1954 Автор
              09.06.2019 21:10
              +1

              Класс!!! +100


            1. bopoh13
              10.06.2019 18:22

              Почему работа с API вам кажется лучше, чем работа с исключениями?


              1. Dragokas
                10.06.2019 18:40

                Я уже не совсем точно помню деталей работы исключений. Но постараюсь объяснить на пальцах.
                Возбуждение исключений инициирует кучу ненужных вызовов (попытку вызова обработчика и т.п.), что ведёт в том числе к замедлению работы программы.
                Эти исключения затем попадают в список, когда исследуешь причины падения, отлаживаешь программу через ProcDump и др., в итоге видишь винегрет, где придётся разбираться, какое из исключений критическое, а какое нет. При массовом использовании такой функции получаешь просто нереальное число мусора в отладке.
                В более сложных сценариях, без нормальной проверки указателей на объекты, используя именно такой способ (понадеясь на мощь On Error Resume Next) можно нарваться на краш.
                P.S. Ну и я бы не назвал это работой с API. Просто разбор полей родной VBA-шной структуры.


                1. bopoh13
                  10.06.2019 20:23

                  Переписанный хук из начала ветки для динамического массива:

                  Dim AddArray() As String
                  
                  If Not (Not Not AddArray) > 0 Then
                    ' If 0 = AryItems(AddArray) Then Exit Sub
                    ' Erase AddArray <-
                  Else
                    ' ReDim AddArray(0 To 1) As String <-
                  End If


    1. catstail1954 Автор
      09.06.2019 08:59

      Кстати, никакого nil в VBA нет.


  1. DatUser
    09.06.2019 21:08

    «Вот так, с помощью нехитрых приспособлений буханку белого (или черного) хлеба можно превратить в троллейбус… Но зачем?»

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


    1. catstail1954 Автор
      09.06.2019 21:09

      Не думаю, что «хайп пройдет»… Не проходит с середины 80-х годов прошлого века.