Примерчик использования LINQ-а на Nemerle
От: VladD2 Российская Империя www.nemerle.org
Дата: 24.12.10 19:37
Оценка: 15 (2)
#Имя: FAQ.nemerle.LinqCombinatorsExample
В порыве акта восстановления справедливости в Интернете
Автор: VladD2
Дата: 24.12.10
написал небольшой примерчик демонстрирующих динамическое формирование Linq-запросов. По сути — это демонстрация использования комбинаторной техники (когда нужные функции формируются путем комбинирования других имеющихся функций). Пример содержим не мало интересного для новичков. Я постарался снабдить его комментариями. Но если все же чего-то не ясно, может задавать вопросы.

Надеюсь, этот пример поможет в освоении немерла.

using Nemerle.Collections;

using System;
using System.Collections.Generic;
using System.Console; // В Nemerle можно открывать не только пространства имен, но и классы!
using System.Linq;

using PersonQuery; // В Nemerle можно открывать не только пространства имен, но и классы!

[Record] // Этот макрос генерирует коструктор инициализирующий все поля класса
class Person
{
  public Name     : string;
  public Birthday : DateTime;
  public Company  : string;
  
  public override ToString() : string
  { // $-строка - это песня! :)
    $"Person: $Name, $(string(' ', 4 - Name.Length)) $(Birthday.ToShortDateString()), $Company"
  }
}

module Program
{
  data : list[Person] = 
    [
      Person("Вася", DateTime(1976, 5, 15),  "RSDN"),
      Person("Петя", DateTime(1967, 3, 1),   "Мега"),
      Person("Петя", DateTime(1981, 1, 5),   "Мега"),
      Person("Петя", DateTime(1953, 1, 12),  "Мега"),
      Person("Аня",  DateTime(1981, 12, 21), "Мега"),
      Person("Дуня", DateTime(1982, 12, 21), "Мега"),
    ];

  Main() : void
  {
    def print(query, msg)
    {
      WriteLine($"$msg:");
      WriteLine($<#..$(query; "\n")#>);
      WriteLine();
    }
    
    def myQuery = data.Where(p => p.Birthday < DateTime(1982, 1, 1)); // эмулируем основной запрос.
    print(myQuery, "Исходный запрос");
    
    def orderingInfos = [("name", true), ("birthday", false)];
    def filterInfos = [("name", "starts", "Пе"), ("birthday", ">=", "10.01.1955")];
    
    def filteredQuery = FilterInfoToQuery(myQuery, filterInfos);
    print(filteredQuery, "После добавления фильтра");

    def orderedMyQuery = OrderingInfosToQuery(filteredQuery, orderingInfos);
    print(orderedMyQuery, "После добавления сортировки");

    def orderedMyQuery2 = OrderingInfosToQuery(myQuery, orderingInfos);
    print(orderedMyQuery2, 
     "После добавления сортировки к исходному запросу (демонстрирвет, что сортировка "
     "производится по двум полям)"); // кстати, идущие подряд строки автоматически конкатинируются
   _ = ReadLine();
  }
}

/// Модуль предоставляющий функции конвертации опписаний заданных в виде
/// в соответствующие кобмбинатроные функции.
module PersonQuery
{
  // Таблицы отображения.

  // Хэш-таблицы, как и любые другие типы коллекций, могут хранить ссылки на функции. 
  // В данном случае - на лямбды (анонимные функции).
  _personFieldOperMap : Hashtable[string * string, Person * string -> bool];
  _orderFirstMap  : Hashtable[string * bool, IEnumerable[Person]        -> IOrderedEnumerable[Person]];
  _orderFollowMap : Hashtable[string * bool, IOrderedEnumerable[Person] -> IOrderedEnumerable[Person]];
  
  this()
  {
    // Заполняем таблицы отображения...
    // Вот эту хрень любо-дорого заполнить на макросах!

    // Указывать параметры типа у коллекций (да и других типов) обычно нет никакого смысла.
    // Они будут автоматически выведены или из определения полей, или из использования коллекций.
    _personFieldOperMap = Hashtable();
    // при использовании кортежей в качестве параметров функций и индексеров имеющих один аргумент
    // можно опускать скобки. Так что запись _personFieldOperMap["birthday", ">="] эквивалентна 
    // записи _personFieldOperMap[("birthday", ">=")]
    _personFieldOperMap["birthday", ">="]     = (p, value) => p.Birthday >= DateTime.Parse(value);
    _personFieldOperMap["birthday", "=="]     = (p, value) => p.Birthday == DateTime.Parse(value);
    _personFieldOperMap["birthday", "<="]     = (p, value) => p.Birthday <= DateTime.Parse(value);
    _personFieldOperMap["company",  "=="]     = (p, value) => p.Company == value;
    _personFieldOperMap["company",  "starts"] = (p, value) => p.Company.StartsWith(value);
    _personFieldOperMap["name",     "=="]     = (p, value) => p.Name == value;
    _personFieldOperMap["name",     "starts"] = (p, value) => p.Name.StartsWith(value);

    _orderFirstMap = Hashtable();
    // "_.Name" - это синтаксис частичного применения. Это аналогично коду "p => p.Name"
    _orderFirstMap["name",     true]  = query => query.OrderBy(_.Name);
    _orderFirstMap["name",     false] = query => query.OrderByDescending(_.Name);
    _orderFirstMap["birthday", true]  = query => query.OrderBy(_.Birthday);
    _orderFirstMap["birthday", false] = query => query.OrderByDescending(_.Birthday);
    _orderFirstMap["company",  true]  = query => query.OrderBy(_.Company);
    _orderFirstMap["company",  false] = query => query.OrderByDescending(_.Company);

    _orderFollowMap = Hashtable();
    _orderFollowMap["name",     true]  = query => query.ThenBy(_.Name);
    _orderFollowMap["name",     false] = query => query.ThenByDescending(_.Name);
    _orderFollowMap["birthday", true]  = query => query.ThenBy(_.Birthday);
    _orderFollowMap["birthday", false] = query => query.ThenByDescending(_.Birthday);
    _orderFollowMap["company",  true]  = query => query.ThenBy(_.Company);
    _orderFollowMap["company",  false] = query => query.ThenByDescending(_.Company);
  }

  /// Формирует LINQ-запрос фильтрующий данные в исходном запросе.
  /// query - исходный запрос.
  /// filteringInfos - список котежей описывающих фильрацию. 
  ///   Первое поле - имя поля в Person. 
  ///   Второе поле - бинарный оператор используемый для создания фильтрующего предиката.
  ///   Третье поле - значение используемое для сравнения (в предикате).
  public FilterInfoToQuery(query : IEnumerable[Person], filteringInfos : list[string * string * string]) : IEnumerable[Person]
  {
    // Если локальная функция или лямбда получает одним из аргументов кортеж,
    // можно произвести его декомпозицию прямо в определении параметра.
    // Конструкция - (field, op, value) - позволяет получить доступ к отдельным полям кортежа.
    def addFilter((field, op, value), query)
    {
      def predicate = _personFieldOperMap[field, op];
      // predicate(_, value) - это частичное применение функции predicate. 
      // Параметр содержащий "_" остается "свободным" (не связывается со значением и превращается 
      // в параметр новой функции). В итоге из функции predicate (имеющей тип Person * string -> bool)
      // Мы получаем новую функцию Person -> bool, которую и передаем в метод Where.
      query.Where(predicate(_, value))
    }

    // Производим свертку filteringInfos и формирование запроса.
    // Свертка последовательно применяет переданную ей функцию ко всем элементам исходной
    // последовательности. Цель свертки вычислить некоторое значение. В нашем случае новый Linq-запрос.
    filteringInfos.FoldLeft(query, addFilter)
  }

  /// Формирует LINQ-запрос сортирующий данные в исходном запросе.
  /// query - исходный запрос.
  /// orderingInfos - список котежей описывающих сортировку. 
  ///   Первое поле - имя поля в Person. 
  ///   Второе поле - сортировка по возрастанию (true) или убыванию (false).
  public OrderingInfosToQuery(query : IEnumerable[Person], orderingInfos : list[string * bool]) : IEnumerable[Person]
  {
    // Типы в локальных функциях можно указывать явно, а можно не указывать.
    // Если типы не указаны, компилятор выведет их самостоятельно.
    def addOrdering(orderingInfo, query)
    {
      def addOrder = _orderFollowMap[orderingInfo]; // получаем функцию добавляющую к запросу сортировку
      def newQuery = addOrder(query); // трансформируем запрос
      newQuery
      // Разбиение на стадии и использование промежуточных переменных сделаны
      // чтобы более наглядно продемонстрировать, что делает этот метод.
      // Код этого метода вполне можно было бы записать в одну строку:
      // _orderFollowMap[orderingInfo](query)
    }
    
    def orderedQuery = 
      match (orderingInfos)
      {
        | [] => query // в случае пустого списка фильтрации возвращ исходный запрос.
        | [first] => _orderFirstMap[first](query) // если в списке только один элемент, добавляем тольтко OrderBy
        | first :: tail => tail.FoldLeft(_orderFirstMap[first](query), addOrdering) // иначе добавляем OrderBy для первого элемента и ThenBy для последующих.
      };
    
    orderedQuery // Возвращаем сформированный запрос как возвращаемое значение функции.
  }
}


Консольный вывод:
Исходный запрос:
Person: Вася,  15.05.1976, RSDN
Person: Петя,  01.03.1967, Мега
Person: Петя,  05.01.1981, Мега
Person: Петя,  12.01.1953, Мега
Person: Аня,   21.12.1981, Мега

После добавления фильтра:
Person: Петя,  01.03.1967, Мега
Person: Петя,  05.01.1981, Мега

После добавления сортировки:
Person: Петя,  05.01.1981, Мега
Person: Петя,  01.03.1967, Мега

После добавления сортировки к исходному запросу (демонстрирвет, что сортировка производится по двум полям):
Person: Аня,   21.12.1981, Мега
Person: Вася,  15.05.1976, RSDN
Person: Петя,  05.01.1981, Мега
Person: Петя,  01.03.1967, Мега
Person: Петя,  12.01.1953, Мега


К слову, о том где действительно рулит MP в виде макросов. Создание модулей вроде PersonQuery можно легко автоматизировать с помощью макросов. На вход он может получать описание класса, а на выходе выдавать модуль ИмяКлассаQuery содержащий все нужные вспомогательные методы. При этом можно даже генерировать как IEnumerable- и IQueryable-версию модуля.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.