Примеры использования языкоориентированного программирования

    Идея language oriented programming (LOP), состоит в том, что во время разработки программы, постоянно создаются миниязыки. Они могут как расширять основной язык разработки, так и быть отдельными языками. Лучшим языком для LOP является Common Lisp с его макросами, но здесь речь пойдёт не о нём. Примеры использования LOP с Common Lisp советую посмотреть в замечательной книге Peter Seibel Practical Common Lisp. Я считаю, что LOP один из самых простых и эффективных способов программирования. Мы описываем задачу и предметную область на самом подходящем для этого языке, а потом стараемся его реализовать.

    Я разрабатываю браузерные игры на Ruby, поэтому часто использую LOP, как для расширения языка и встроенных DSL (Ruby позволяет делать это очень хорошо), так и для создания миниязыков связанных со сложной игровой механикой. В этой статье я рассмотрю простое расширение основного языка, встроенный мини-DSL и два не встроенных языка. Буду приводить примеры в близкой мне тематике, надеюсь они будут вполне понятны.



    Статью написал хабраюзер CrazyPit, но ему не хватает кармы для опубликования.

    Простое расширение языка



    Мне часто необходимы классы, которые выполняют определённый алгоритм, работая над объектом. Конечно можно все методы занести в основной класс, но это будет излишнее засорение. К тому же, алгоритм может использовать несколько методов и исключений, которые совершенно не нужны в основном классе.

    У игрока есть несколько им составленных дек (набора карт, которыми игрок использует в коллекционной карточной игре). Нужно выбрать одну из дек — активировать. Для этого я создаю отдельный класс, реализующий алгоритм активации.

    Deck::Activator = Struct.new(:deck)
    class Deck::Activator
    
       def activate!
         ......
       end 
    
       private
       <some helper methods and exceptions>
    end
    


    Можно этот модуль использовать так:

    Deck::Activator.new(some_deck).activate!
    
    


    Но гораздо красивее:

    deck.activator.activate!
    
    # или вот так
    
    deck.activate!
    


    Для этого нужно добавить метод в класс Deck:

    class Deck
    
      def activate!
         Deck::Activator.new(some_deck).activate!
      end
    
    end
    


    Но поскольку таких алгоритмов достаточно много, я сделал небольшое улучшение, это класс-метод strategy (не имеет прямого отношение к одноимённому паттерну). Теперь я делаю так:

    class Deck < ActiveRecord::Base
      strategy :activator
    end
    
    Deck.find(:first).activator.activate!
    
    


    или:

    class Deck < ActiveRecord::Base
      strategy :activator, :delegate => [:activate!]
    end
    
    Deck.find(:first).activate!
    
    


    Имя класса алгоритма по умолчанию это <имя класса где вызывается метод>::<имя задаваемое в strategy>. Но может быть задано вручную (:class => BrainDestructor).

    Таких расширений много, как в самом руби, так и в RoR. Думаю каждый, кто много программировал на Ruby, делал что-то подобное.

    Встроенный DSL для обозначения ограничений



    В игре есть различные комнаты, куда пускают не всех, а только если игрок соответствует определённым правилам. Например его уровень больше 10, или кол-во карт в деке не больше 8. Такие ограничению комбинируются. Есть виды ограничений, например «Уровень игрока >= N» и есть конкретные ограничения «Уровень игрока >= 13».

    Для задания видов ограничений можно использовать DSL define_constraint, и потом хранить ограничение и их комбинации в базе данных.

    
      define_constraint "deck_sum_between", "Сумма уровней карт между %N и %M" do
        (arguments['N'].to_i..arguments['M'].to_i).include?(context.deck.sum_of_card_levels)
      end
    
    
      define_constraint "deck_without_duplicates", "Дека без дублей" do
        !context.deck.has_duplicates?
      end
    
      define_constraint "user_level_ge", "Уровень игрока %X  или выше" do
        context.level >= arguments['X'].to_i
      end
    
    


    В каждом виде ограничение мы задаём его имя (deck_sum_between), описание «Сумма уровней карт между %N и %M», из которого потом, на основание параметров, получается описание конкретного ограничения. И конечно реализацию ограничения, которая должна возвращать true, если игрок или другой объект подходит под ограничение. Система универсальна поэтому не user а context.
    В итоге ограничения можно записать так deck_sum_between(N=>10, M=> 20) или хранить имя и параметры в разных свойствах объекта.

    Язык логическое выражение из ограничений



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

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

    Например: уровень игрока > 10 И размер деки <= 8 карт И (карты игроки из кланов 1,2,3 ИЛИ карты игрока из кланов 4,5,).
    Создан язык выражений, которое это задаёт (тут использован слегка другой вид базовых ограничений, чем в предыдущем разделе):

    (AND user_level_ge(12) 
         deck_size_le(8)
         (OR deck_has_only_clans(1,2,3) 
             deck_has_only_clans(4,5,6)))
    


    Я использовал немного lisp-style чтобы было удобнее со списками ограничений включающих большой список И.

    Далее сохраняем это строчку в модели Room (room.restrictions_string). Во время, когда нам нужно вычислить ограничение парсим строку, вычисляем все базовые ограничения а также общий результат и отдаём клиенту. Игрок видит, необходимые условия и какие из них он не прошёл.

    Язык описания правил бустера



    Бустер — это продаваемый набор карт в коллекционной карточной игре, в которой входят несколько случайных карт по определённым правилам. Например 5 средних карт жителей болот и одна хорошая.

    Каждое из правил генерации карт можно описать текстом:

    rarity(1) — плохая карта (кол-во задаётся вне правила, об этом позже)
    rarity(1)|clans(6,7,8) — одна плохая карта из кланов 6,7,8. "|" здесь символизирует конвейер unix, а не логическое или.

    Также возможны правила с вероятностью:

    clans(1,2,3)|expectance(1,60,2,38,3,2) — карты из клана 1, 2 или 3; с вероятностью 60% — плохая, с вероятностью 38% — средняя, с вероятностью 3% — хорошая.

    Каждое правило реализуются на основе ActiveRecord механизма scope примерно так:

      def rule_clans(scope, ids)
        scope.scoped_by_clan_id(ids)
      end
      
      def rule_expectance(scope, params)
        scope.scoped_by_rarity(Expectance.for_expectance(Hash[*params.map(&:to_i)]))
      end
    

    Кстати, здесь тоже можно было расширить язык и сделать описание чуть более лаконичным.

    Правил объединяются reduce:

      def generate_card_original
        rules_scope = rules.reduce(Card::Original) do |scope, rule|
          rule.add_scope(scope)
        end
        rules_scope.randomly.find(:first)
      end
    


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

    Остаётся вопрос как генерировать скажем 5 карт по одному правилу и 2 по другому. Есть несколько вариантов, я использовал такое класс Booster has_many generators. В объекте класса Generator храниться кол-во карт и правило, по которому каждая из карт генерируются. Но можно усложнить базовый язык и записывать все правила бустера одной строкой:
    5[rarity(1,2)], 2[clans(1,2)|expectance(1,60,2,40)]


    Заключение



    Я привёл примеры использования LOP в повседневной практике. Многие используют DSL даже не подозревая об этом (XML-задание интерфейсов например). Но создают свои DSL только небольшое кол-во разработчиков. Надеюсь эта статья подтолкнёт вас к подробному изучению вопроса.
    Поделиться публикацией

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 18

      +1
      Кстати. Вы не пробовали пользоваться MPS (http://www.jetbrains.com/mps/index.html)?
      Если да, скажите пару слов?

      (я в теме лишь слегка, писал DSL для Java)
        +1
        Не использовал, всё хочу посмотреть, но пока руки не доходят…
          +1
          Что то я не пойму — это ваш alter ego написал?
            0
            Не очень понял вопрос. Если то что статья не от моего имени, то там объясняется в начале, что у меня не было кармы когда писал, поэтому помог добрый человек.
              0
              Теперь да. А без этой надписи смотрелось странно.
                0
                Надпись об авторстве присутствовала с момента создания топика ;)
          0
          Я пробовал, _очень_ понравилось.
          +1
          Для заинтересованных Practical Common Lisp — есть перевод, сделанный энтузиастами:
          pcl.catap.ru
          PDF версию можно найти здесь: lisper.ru/pcl
            0
            Сcылка на pdf не работает :(
          0
          рабочая ссылка:
          lisper.ru/pcl/

          там не только pdf, но и более удобочитаемый html
            +1
            Ооо, коллекцинонные игры… в былой молодости я помнится написал движок для Magic: the Gathering. Было очень интересно, т.к. правила этой игры толком не ложатся ни на один из существующих сегодня языков программирования.
              0
              миниязыки — для миниязыков
                0
                это ваша команда сделала кланз?
                +1
                Здорово, что вы прочитали про Лисп и попробовали что-то подобное в Руби.
                Теперь стоит прочитать про бизнес-правила =)

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

                Пока правил не много, за отсутствием дублирования можно следить глазами, но если правила пишут разные люди или они разбросаны по коду, то становится тяжело — могут появиться конфликты, противоречия, дублирование и т.п. ужасы.

                Например, в Java это решают при помощи jboss.org/drools/

                Чтиво — en.wikipedia.org/wiki/Rete_algorithm
                  0
                  Спасибо, почитаю.
                  0
                  В тему — literate testing: googletesting.blogspot.com/2009/09/tott-literate-testing-with-matchers.html
                  DSL для тестов

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

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