В порыве
акта восстановления справедливости в ИнтернетеАвтор: 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-версию модуля.