📜 ⬆️ ⬇️

Examples of using language-oriented programming

The idea of ​​language oriented programming (LOP), is that during the development of the program, language languages ​​are constantly created. They can either extend the main development language or be separate languages. The best language for LOP is Common Lisp with its macros, but this is not about it. I advise you to look at examples of using LOP with Common Lisp in the wonderful book Peter Seibel Practical Common Lisp . I believe that LOP is one of the easiest and most effective ways to program. We describe the task and the subject area in the most appropriate language for this, and then try to implement it.

I develop browser games for Ruby, so I often use LOP, both for language expansion and embedded DSL (Ruby allows you to do it very well), and for creating mini-languages ​​associated with complex game mechanics. In this article, I will look at a simple extension of the main language, built-in mini-DSL, and two non -built-in languages. I will give examples in topics close to me, I hope they will be quite clear.


')
The article was written by the CrazyPit habraiser , but it lacks the karma to publish.

Simple language extension



I often need classes that perform a certain algorithm when working on an object. Of course, you can put all the methods in the main class, but it will be excessive clogging. In addition, the algorithm can use several methods and exceptions that are completely unnecessary in the main class.

The player has several decks made by him (a set of cards with which the player uses in a collectible card game). You need to select one of the decks - activate. To do this, I create a separate class that implements the activation algorithm.

 Deck :: Activator = Struct.new (: deck)
 class Deck :: Activator

    def activate!
      ......
    end 

    private
    <some helper methods and exceptions>
 end


You can use this module as follows:

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



But much more beautiful:

 deck.activator.activate!

 # or like this

 deck.activate!


To do this, add a method to the Deck class:

 class Deck

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

 end


But since there are quite a lot of such algorithms, I made a slight improvement, this is the class-method strategy (not directly related to the pattern of the same name). Now I do this:

 class Deck <ActiveRecord :: Base
   strategy: activator
 end

 Deck.find (: first) .activator.activate!



or:

 class Deck <ActiveRecord :: Base
   strategy: activator,: delegate => [: activate!]
 end

 Deck.find (: first) .activate!



The class name of the algorithm by default is <class name where the method is called> :: <the name specified in strategy>. But it can be set manually (: class => BrainDestructor).

There are many such extensions, both in Ruby itself and in RoR. I think everyone who programmed a lot in Ruby did something like that.

Built-in DSL to indicate restrictions



The game has different rooms, where not everyone is allowed, but only if the player complies with certain rules. For example, its level is more than 10, or the number of cards in the deck is no more than 8. Such restrictions are combined. There are types of restrictions, for example, “Player Level> = N” and there are specific restrictions “Player Level> = 13”.

You can use the DSL define_constraint to set the types of restrictions, and then store the restrictions and their combinations in the database.

   define_constraint "deck_sum_between", "Sum of map levels between% N and% M" do
     (arguments ['N']. to_i..arguments ['M']. to_i) .include? (context.deck.sum_of_card_levels)
   end


   define_constraint "deck_without_duplicates", "Deca without duplicates" do
     ! context.deck.has_duplicates?
   end

   define_constraint "user_level_ge", "Player Level% X or above" do
     context.level> = arguments ['X']. to_i
   end



In each type of restriction, we specify its name (deck_sum_between), the description of the “Sum of map levels between% N and% M”, from which, on the basis of the parameters, we get a description of a specific restriction. And of course the implementation of the constraint, which should return true if the player or other object fits the constraint. The system is therefore universal not the user but the context.
As a result, the restrictions can be written as deck_sum_between (N => 10, M => 20) or to store the name and parameters in different properties of the object.

Language logical expression of constraints



In game dev, it is often necessary that some rules, on the basis of which the algorithm is formed, are stored dynamically, for example, in the database, so the game algorithms can be changed literally from the admin form. This and the following example describes two such languages.

Sometimes we need not simple constraints that can be implemented simply by a list of basic constraints, but a more complex logical expression.

For example: player level> 10 and deck size <= 8 cards AND (player cards from clans 1,2,3 OR player cards from clans 4,5,).
An expression language has been created that defines this (here we used a slightly different kind of basic constraints than in the previous section):

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


I used a bit of lisp-style to make it more convenient with lists of restrictions including a large list of I.

Next, we save this line in the Room model (room.restrictions_string). At the time when we need to calculate the constraint by parsing a string, we calculate all the basic constraints as well as the overall result and give it to the client. The player sees the necessary conditions and which of them he did not pass.

Booster Rule Description Language



A booster is a salable card set in a collectible card game that includes several random cards according to certain rules. For example, 5 medium maps of swamp residents and one good.

Each of the map generation rules can be described with the text:

rarity (1) - bad card (number is given outside the rule, more on that later)
rarity (1) | clans (6,7,8) - one bad card from clans 6,7,8. "|" here symbolizes the unix pipeline, not logical or.

Rules with probability are also possible:

clans (1,2,3) | expectance (1,60,2,38,3,2) - cards from clan 1, 2 or 3; with a probability of 60% - bad, with a probability of 38% - medium, with a probability of 3% - good.

Each rule is implemented based on the scope's ActiveRecord mechanism, something like this:

   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

By the way, here, too, it was possible to expand the language and make the description a little more concise.

The rules are combined 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


As a result, we get one request for one card, regardless of the complexity of the rule.

The question remains how to generate, say, 5 cards according to one rule and 2 according to another. There are several options, I used this class Booster has_many generators. The number of cards and the rule by which each of the cards are generated is stored in the object of the Generator class. But you can complicate the base language and write all the rules of the booster in one line:
  5 [rarity (1,2)], 2 [clans (1,2) | expectance (1,60,2,40)] 


Conclusion



I gave examples of using LOP in everyday practice. Many use DSL without even knowing it (XML-task interfaces for example). But create their DSL only a small number of developers. I hope this article will push you to a detailed study of the issue.

Source: https://habr.com/ru/post/71557/


All Articles