Функциональные интерфейсы… в 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=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

      +2
      Моя программерская карьера 14 лет назад началась с того, что я два года не вылезал из экселя и VBA… Аж всплакнул от ностальгии.
      Запомню на всякий случай, вдруг случится необходимость опять с VBA столкнуться.
        –1
        VBA это конечно «шедевр». Положу в закладки рядом с проверкой массива Dim a() As String на на nil.
        If (Not a) <> (Not 0) Then
          ub=UBound(a)
        End if
        

        Но всё таки может лучше сразу написать интерпретатор нормального языка на VBA и писать на нём.
          0
          Что за трешовый код? Проверку на инициализацию надо делать так

          Public Function IsArrayInitialized(arr) As Boolean
            On Error Resume Next
            Dim rv As Long: rv = UBound(arr)
            IsArrayInitialized = (Err.Number = 0)
          End Function 
            0
            Незнаю что более трешовое on error resume next или (not a)<>-1
              –1
              За то я знаю. Ваш код ужасен. Если так нужно использовать проверку на инициализацию массива включайте в свои проекты функцию IsArrayInitialized(). Примеров ее реализации различными путями множество. И кстати да, как уже было сказано никакого nil — в VBA нет.
                +1
                Я тоже считаю, что исполнение кода через возбуждение ошибки — это плохой тон. Поэтому проверяю внутреннюю структуру SAFEARRAY: AryItems()
                  +1
                  Класс!!! +100
                    0
                    Почему работа с API вам кажется лучше, чем работа с исключениями?
                      +1
                      Я уже не совсем точно помню деталей работы исключений. Но постараюсь объяснить на пальцах.
                      Возбуждение исключений инициирует кучу ненужных вызовов (попытку вызова обработчика и т.п.), что ведёт в том числе к замедлению работы программы.
                      Эти исключения затем попадают в список, когда исследуешь причины падения, отлаживаешь программу через ProcDump и др., в итоге видишь винегрет, где придётся разбираться, какое из исключений критическое, а какое нет. При массовом использовании такой функции получаешь просто нереальное число мусора в отладке.
                      В более сложных сценариях, без нормальной проверки указателей на объекты, используя именно такой способ (понадеясь на мощь On Error Resume Next) можно нарваться на краш.
                      P.S. Ну и я бы не назвал это работой с API. Просто разбор полей родной VBA-шной структуры.
                        0
                        Переписанный хук из начала ветки для динамического массива:
                        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
              Кстати, никакого nil в VBA нет.
              0
              «Вот так, с помощью нехитрых приспособлений буханку белого (или черного) хлеба можно превратить в троллейбус… Но зачем?»

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

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

              Самое читаемое