C# 3.0 и LINQ: для тех, кто ещё не в курсе

Что было бы, если проснувшись утром, вы обнаружили в языке, на котором говорите, возможность декларативно описывать те вещи, которые вы хотите получить? На самом деле, в русском языке существует такая возможность, и мы пользуемся ей ежедневно. Но на практике она не всегда работает: мы приходим в магазин и говорим: «Дайте мне книги Дональда Кнута, вышедшие позже 1970 года, в заголовке которых встречается слово программирование». Наверняка, продавец косо на нас посмотрит (хотя, всё зависит от магазина), подведёт к книжной полке и предложит самим найти то, что нам требуется :).

Хотя не исключено и то, что он запишет наши требования и удалится куда-то к белому ящику с монитором, и, вернувшись, вручит стопку нужных нам книг. Этот случай будет означать то, что продавец понял тот запрос, что мы сформулировали ему на родном (native) языке, затем он транслировал его в какой-то свой ментальный формат и воспользовался каким-либо средством для поиска и выборки наших данных. Нам не пришлось самим искать эти книги на полке, а так же не пришлось брать листик бумаги и расписывать на нём SQL-запрос, или же разучивать специальные термины, используемые в культуре поиска книг. Мы воспользовались расширением своего родного языка, чтобы получить необходимую выборку.

Тут может возникнуть проблема «чистоты»: нужно ли загромождать чистый и простой язык новыми сомнительными конструкциями, вместо того, чтобы использовать для этих целей другой, специально для этого предназначенный? Разработчики .NET всё-таки решили добавить в C# и другие языки платформы .NET проблемно-ориентированное расширение LINQ, интегрированный язык запросов.

И теперь мы можем писать так:

var books = from b in all_books
            where b.Author == "Donald Knuth"
&& b.Title.ToLower().Contains("programming")
&& b.DatePublished.Year > 1970
            select new { b.Title, b.ISDN };

Причём такие запросы можно выполнять к любым объектам, реализующим IEnumerable. Для незнакомого с LINQ человека это может выглядеть непонятно, но я думаю, к концу статьи всё станет на свои места. Конечно, данная конструкция кажется неуместной в контексте обычных конструкций языка C#. Однако, использовать такой синтаксис или нет – дело конкретного случая. Аналогичные действия можно выполнить и с помощью «родного» синтаксиса С#, но они не будут выглядеть так впечатляюще :). На самом деле всё это транслируется в стандартные конструкции перед выполнением, так что кто-то может заметить, что перед нами синтаксический сахар в чистом виде.

Немного про LINQ

Идеи, появившиеся в новой версии C#, позаимствованы из экспериментального языка , который специально был создан для улучшения обработки XML и реляционных данных в языке C#. Так же некоторое сходство можно проследить с функциональным языком F# (потомок OCaml).

Расширения LINQ представлены в С# 3.0. Всё необходимое для работы с основными возможностями LINQ находится в пространстве имён System.Linq. Стоит отметить, что Microsoft представила целый набор новых технологий на базе LINQ для использования с базами данных, XML, объектами, реализующими IEnumerable. Технологии LINQ to Databases, LINQ to XML и LINQ to Entities выходят за рамки данной статьи.

Нам понадобится Visual Studio 2008, либо Visual Studio 2005 с установленным .NET Framework 3.5 и расширениями LINQ. Либо же, вы можете совершенно бесплатно загрузить Visual C# 2008 Express Edition отсюда.

Итак, начнём.

Что за декларативность и функциональность?

Работая с .NET, мы работаем с объектно-ориентированной средой, и ООП-языками: C#, VB.NET, и другими. В них мы оперируем классами, объектами, пространствами имён и так далее. ООП отлично справляется с отдельными объектами, однако, когда дело доходит, например, до коллекций объектов, всё усложняется. Коллекции гораздно тяжелее обрабатывать. Для этого было написано много вспомогательных классов. Но когда нам требуется сотворить нетривиальную операцию над коллекциями, мы заходим в лабиринт загадочных нагромождений излишнего кода.

LINQ пытается решить проблему сложных манипуляций, привлекая сильные стороны функционального программирования. Мы говорим, что функция – это объект, позволяем передавать её как параметр в другие функции, возвращать в качестве значения, хранить массивы из функций. Такие функции назвали λ-функциями. Для них предусмотрен свой синтаксис, однако, на самом деле они являются чем-то похожим на анонимные делегаты. Более того, мы можем передавать λ-функциям в качестве параметра другие функции, таким образом организовывая функции высших порядков. Берём нашу коллекцию (или несколько коллекций), которая реализует IEnumerable<T>, пропускаем через цепочку λ-функций, и на выходе получаем то, что нам необходимо.

LINQ пытается решить проблемы манипуляций с данными вообще. Посмотрите, бизнес-объекты в ООП, и данные в реляционной БД (или XML) имеют разную природу, но им нужно как-то согласовываться. Этим «мостиком» и может служить LINQ. Мы строим декларативные запросы в своём языке, которые хранятся в виде «деревьев выражений», и исполняем их, когда нам это потребуется.

Для того, чтобы получить всё это в нефункциональном языке, требуется добавить в него множество расширений. А так же некоторые синтаксические «штуки», которые облегчат кодирование. Всё это будет рассмотрено далее. Это именно то, что появилось в C# 3.0.

Вывод типа

Вывод типа (type-inference) – возможность, широко используемая в динамически-типизируемых языках. Идея состоит в том, что мы можем не указывать тип переменной, ссылающейся на объект, если этот тип может быть однозначно определён из значения (или выражения). Мы можем писать так:

var s = "here i am!";
var i = 123;
var varlist = new List<double>();

Конечно помимо того, что этот синтаксис сокращает длину кода, он обладает и другими преимуществами, о которых будет рассказано позже. Согласитесь, удобнее вместо

MySuperLongClass<MyLongClass<int, string>> obj =     new MySuperLongClass<MyLongClass<int, string>>()

Писать

var obj = MySuperLongClass<MyLongClass<int, string>>()

И тип переменной будет автоматически выведен из выражения. Однако при всём этом, С# остаётся строгим статически-типизируемым языком, и мы не можем позже присвоить нашей переменной объект другого типа.

Проверить, какой тип нам вывел компилятор, мы можем так:

Console.WriteLine(s.GetType());
Console.WriteLine(i.GetType());
Console.WriteLine(varlist.GetType());

Как видно, мы получили именно то, чего и ожидали:

System.String
System.Int32
System.Collections.Generic.List`1[System.Double]

Ограничение состоит в том, что тип выводится только если это можно сделать однозначно. Поэтому мы не сможем написать

var obj = null

Анонимные типы

Анонимные типы позволяют создавать экземпляры классов, которые мы не определяли ранее. Если нам «здесь и сейчас» нужен экземпляр какого-то класса, имя которого нам не важно, а важен только сам объект (и его свойства), мы воспользуемся следующей конструкцией:

var anonim = new { Name = "Inkognito", Age = 150 };

Вот что мы получим в итоге:

<>f__AnonymousType0`2[System.String,System.Int32]

Здесь нам и пригодился вывод типа, поскольку иначе работа с анонимными классами была бы затруднительна. В фигурных скобках указываются свойства и их значения. Теперь мы можем использовать этот объект, как и любой другой:

Console.WriteLine("My name is {0}, and i am {1} years old!",
                  anonim.Name, anonim.Age);

Методы расширения

Идея возможности расширять классы новыми методами без использования наследования не нова. Например, такую же возможность можно встретить в Ruby в виде примесей (mixins). Смысл в том, что мы пишем метод вне зоны определения класса, который мы собираемся расширять, а потом можем использовать этот метод как «родной» для этого класса. При этом мы можем расширять классы, для которых у нас нет исходного текста, нам он не требуется, поскольку методы расширения помещаются в так называемых статических классах, которые предназначены специально для хранения таких методов.

Допустим, нам очень не хватает в стандартном классе String метода, который бы преобразовывал нашу строку в строку, написанную ЗаБоРоМ. Мы определяем статический класс, в котором будет содержаться необходимый нам метод:

public static class Util
{
    public static string Zaborom(this string s)
    {
          StringBuilder builder = new StringBuilder();
          bool upper = true;
          foreach (char c in s)
          {
              if (upper)
                 builder.Append(c.ToString().ToUpper());
             else
                 builder.Append(c.ToString().ToLower());
             upper = !upper;
          }
          return builder.ToString();
    }
}

Обратите внимание на одну особенность – аргумент метода: this string s. Ключевое слово this в этом контексте как раз и указывает на то, что данный метод является методом расширения для класса string. Мы можем вызывать его следующими способами.

// Обычным, как будто мы вызываем статический метод:
s = "he-he-he! hello world!";
Util.Zaborom(s)                 

// Либо как метод-расширение:
s = "he-he-he! hello world!";
s.Zaborom()                 

// Конечно, так тоже можно :)
"he-he-he! hello world!".Zaborom();

Результат во всех случаях будет аналогичный:

He-hE-He! HeLlO WoRlD!

Хотя методы расширения используются по полной в LINQ, они и сами по себе не менее полезны.

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

Методы расширений позволяют выполнять довольно интересные трюки, например такие:

static class MyUtils
{
    public static int Kilobytes(this int bytes)
    {
        return bytes * 1024;
    }
}                 

// ...                 

5.Kilobytes(); // -> 5120
2.Kilobytes() + 10; // -> 2058

Применительно же к контейнерам, мы можем написать единый вариант функции Count(), подсчитывающий количество элементов в контейнере, следующим образом:

public static class EnumerableUtil
{
       public static int MyCount(this IEnumerable enumerable)
       {
           int count = 0;
           foreach (var e in enumerable)
               count++;
           return count;
       }
}

Автоматические свойства

Часто бывает так, что свойства некоторого объекта не делают ничего, кроме установки и чтения значений private-переменных. В таком случае писать тела таких свойств становится довольно утомительно, особенно если их много. От этой траты временны нас защитить призваны автоматические свойства: их синтаксис похож на описания свойств в интерфейсах, однако смысл этому придаётся другой.

class Person
{
    public Person(string name, string lastname)
    {
        Name = name;
        Lastname = lastname;
    }                 

    public string Name { get; private set; }
    public string Lastname { get; private set; }
    public int Age { get; set; }
    public bool IsProgrammer { get; set;}
}

Get-теры и Set-теры свойств могут объявляться с модификатором private, что означает запрет на использование из вне класса. Теперь можно использовать их, как обычные свойства:

Person me = new Person("Vasya", "Pupkin");
me.Age = 200; // good
me.Name = "Inkognito"; // error! private setter

Инициализация свойств

Ещё одна вещь, которая часто встречается при работе с нашими объектами – это их инстанцирование. И зачастую, именно при создании нового экземпляра класса нам необходимо задать состояние нашего объекта перечнем его свойств. Для этого приходится делать широкие конструкторы со многими параметрами. Или отказаться от этой идеи, и установить некоторые свойства нашего объекта уже после создания (в смысле записи кода).

Инициализация свойств позволяет задавать значения свойств объекта прямо при его создании (имеется в виду запись кода, на самом же деле всё происходит немного по-другому). Вспомнив про класс Person из предыдущего раздела, мы может создать массив людей так:

var persons = new Person[]
{
    new Person(”Harry”, “Hacker”) { Age = 30, IsProgrammer = true },
    new Person(”Vinnie”, “Pooch”) { IsProgrammer = false, Age = 45},
    new Person(”Kolo”, “Bok”) { IsProgrammer = true}
};

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

λ-выражения

λ-выражения (лямбда-выражения) – интересный механизм, который уже долгое время используется во многих языках программирования. Говоря неформально, λ-выражение – это функция, которую можно выполнять, передавать в качестве параметра, возвращать из метода и так далее. На самом деле λ-выражения в некотором виде уже присутствуют в C# 2.0 в виде замыканий анонимных делегатов:

int[] array = new[] { 1, 321, 234, 54, 34 };
var a = array.Where(
      delegate(int elem) { return elem % 2 == 0; }
);                 

foreach (var elem in a)
    Console.Write(elem + ” “);

Получим:

234 54 34

В этом примере используется стандартный метод расширения Where, который выбирает из IEnumerable элементы, соответствующие некоторому условию. Здесь мы использовали анонимный делегат для представления предиката. Однако такая запись достаточно громоздка, и в LINQ был введён дополнительный синтаксис, так что аналогичное можно записать так:

var a = array.Where(elem => elem % 2 == 0);

На самом деле эта конструкция есть экземпляр Func<int,bool>. Func инкапсулирует метод, его аргументы и возвращаемое значение. Так, например, метод расширения Where мог быть реализован так:

public static IEnumerable Where<T>(
            this IEnumerable<T> enumerable,
            Func<T, bool> condition )
{
    foreach (T e in enumerable)
        if (condition(e))
            yield return e;
}

Func же может представляет из себя следующее:

delegate R MyFunc<T, R>(T arg);

С помощью λ-выражений в купе с методами расширения тоже можно развлечься следующим образом:

static class MyUtils
{
        public delegate void TimesDelegate(int i);
        public static void Times (this int n, TimesDelegate func)
        {
            for (int i = 0; i < n; i++)
                func(i);
        }
}                 

// ...                 

5.Times(
    i => Console.WriteLine(i)
);

Ленивые вычисления

Идея ленивых (lazy evaluation) или отложенных вычислений тоже родом из функциональных языков, а особенно из «чистых» функциональных языков. Чистые языки – это такие, в которых отсутствуют побочные эффекты вроде тех, что, при вызове одного и того же метода с одними и теми же параметрами несколько раз, он может возвращать разные значения. Большинство языков являются не чистыми. Даже более того, мы настолько привыкли к этому явлению, что и не замечаем его. Однако данная «нечистота» негативно сказывается на многих вещах: программы сложнее писать, сложнее отлаживать, поскольку наши методы могут вести себя непредвиденно. Однако рассмотрение отложенных вычислений и их отношение к чистоте языков программирования выходят за рамки данной статьи. C# не чистый, и даже в полной мере не функциональный язык, так что ленивые вычисления в нём могут использоваться лишь в ограниченном круге задач.

Не стоит думать, что раз вычисления ленивые, то их нужно уговаривать выполнить их работу. Согласно идее ленивых вычислений, вычисления следует откладывать до того момента, пока их результат действительно не понадобится. В императивных языках часто бывает так, что мы вычисляем некоторое значение, а потом оно никогда не используется – время тратится зря. На самом деле какая-то часть «ленивости» присутствует в некоторых языках, например, C++, при проверке условий вроде

If (a == true && b == 123)

Если a принимает значение false, то условия для b не проверяется, поскольку не влияет на результат.

Вернёмся к C#. На самом деле мы уже использовали ленивость в предыдущих разделах, когда рассматривали λ-выражения и методы расширения. Вспомним пример с нахождением чётных чисел:

var a = array.Where(elem => elem % 2 == 0);

Дело в том, что в этой строчке вычисления не производятся. Они откладываются до того момента, пока значения нам действительно не понадобятся. Например, реальные вычисления будут производится неявно здесь:

foreach (var elem in a)
       Console.Write("{0}, ", elem);

Увидеть это можно, внеся изменения в Where (Не забудьте поместить его в какой-нибудь статический класс):

public static IEnumerable MyWhere<T>(
            this IEnumerable<T> enumerable,
            Func<T, bool> condition )
{
    foreach (T e in enumerable)
        if (condition(e))
        {
            Console.Write("(Мы в фильтре)");
            yield return e;
        }
}

Теперь следующий код

int[] array = new[] { 1, 321, 234, 54, 34 };           

Console.WriteLine(”Фильтруем”);
var lazy = array.MyWhere(elem => elem % 2 == 0);            

Console.WriteLine(”Распечатываем”);
foreach (var elem in lazy)
       Console.Write(”{0}, “, elem);

Даст такой вывод:

Фильтруем
Распечатываем
(Мы в фильтре)234, (Мы в фильтре)54, (Мы в фильтре)34,

Здесь мы уже подходим вплотную к запросам LINQ. Отложенные вычисление как раз и позволяют связывать запросы в цепочку, и только потом рассчитывать.

Запросы

Вооружившись знанием из предыдущих разделов, напишем LINQ-запрос. Возьмём знакомый нам массив людей:

var persons = new Person[]
{
    new Person(”Harry”, “Hacker”) { Age = 30, IsProgrammer = true },
    new Person(”Vinnie”, “Pooch”) { IsProgrammer = false, Age = 45},
    new Person(”Kolo”, “Bok”) { IsProgrammer = true}
};

И найдём полные имена и возраст тех, кто является программистом, отсортируем по возрасту (не стоит забывать, что аналогичный запрос работал бы и с любой коллекцией из Person, которая реализует IEnumerable<Person>):

var programmers = persons.Where(p => p.IsProgrammer)
        .Select(p => new { Fullname = p.Name + " " + p.Lastname, p.Age })
        .OrderByDescending(p=>p.Age);

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

Console.WriteLine("Programmers: ");
foreach (var p in programmers)
    Console.WriteLine("{0}, {1} years old", p.Fullname, p.Age);

Примечательно, мы тут мы можем использовать специальный SQL-подобный синтаксис, так, предыдущий запрос перепишем так:

var programmers = from p in persons
                  where p.IsProgrammer
                  orderby p.Age descending
                  select new { Fullname = p.Name + " " + p.Lastname,
                               p.Age };

Чего мы и добивались. Однако это не всё, нам доступен весь спектр нужных вещей, например, таких как объединения (join). Допустим, у нас есть следующие классы:

public class Customer
{
      public int Key;
      public string Name;
}                 

public class Order
{
      public int CustomerKey;
      public string What;
}

И массивы (Обратите внимание на запятую после последнего элемента массива, синтаксической ошибки не возникает. Это сделано для того, чтобы было легче добавлять новые элементы в конец):

var customers = new Customer[]
{
    new Customer { Key=1, Name=”Vasya”},
    new Customer { Key=2, Name=”Petya”},
    new Customer { Key=12, Name=”Vova”},
};                 

var orders = new Order[]
{
    new Order { CustomerKey=1, What=”Book” },
    new Order { CustomerKey=1, What=”Clock” },
    new Order { CustomerKey=2, What=”Pen”},
    new Order { CustomerKey=12, What=”Phone”},
};

Вот простой Join-запрос:

var q = from c in customers
        join o in orders on c.Key equals o.CustomerKey
        select new { c.Name, o.What };

Даст нам следующее:

{ Name = Vasya, What = Book }
{ Name = Vasya, What = Clock }
{ Name = Petya, What = Pen }
{ Name = Vova,  What = Phone }

Мы так же можем использовать группировку (group by), аггрегирование (например, Sum(), Count()), и другие функции, полный список которых вы можете найти в документации по LINQ.

Деревья выражений

Как было сказано, в LINQ λ-функции представляються в виде деревьев выражений. Мы можем создавать λ-выражения динамически. Класс Expression<T> (который находится в System.Linq.Expressions) представляет собой такое дерево выражений. Взгляните на следующий пример:

Expression<Func<int, int>> square = p => p * p;

Здесь компилятор сам построил нам дерево выражений для λ-функции. Мы можем откомпилировать его в λ-функцию, и затем использовать:

var square = square_expr.Compile();
square(5); // -> 25

А вот как мы можем построить дерево выражений вручную, используя статические методы из Expression:

ParameterExpression pe = Expression.Parameter(typeof(int), "num");
Expression<Func<int, int>> mysquare =
        Expression.Lambda<Func<int, int>>(
            Expression.Multiply(pe, pe),
            new ParameterExpression[]{ pe}
        );
mysquare.Compile()(5); // -> 25

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

Заключение

С каждой версией C# впитывает всё больше новых расширений. В данной статье было рассмотрено относительно проблемно-ориентированное расширение LINQ, которое помогает легче взаимодействовать с миром данных. Да и сами по себе новые возможности C# 3.0 достаточно полезны. Чтобы идти в ногу со временем, языки эволюционируют, впитывая в себя современные идеи. Наверное, не стоит думать, что то, что принесёт нам LINQ – это решение всех проблем, связанных с манипуляциями данными. Однако данная технология представляется достаточно элегантным «связующим звеном». Интересно будет понаблюдать за тем, как она приживётся, вытерпит ли проверку временем, и во что выльется в дальнейшем.

.NET, CSharp, framework, Functional Programming

Замыкания в Java: что может быть

Как определить – нужна та или иная «фишка» в языке программирования или нет? Можно ли то же самое сделать стандартными средствами, или она настолько необходима, что нужно расширить сам язык новыми конструкциями, тем самим и усложнив его, и упростив?

Появление «настоящих» замыканий в Java может спровоцировать волну новых споров о «чистоте» вроде споров об универсальных типах. Однако здесь снова всё неоднозначно, поскольку замыкания – идея очень простая, однако вместе с тем достаточно мощная.

В этой статье я кратко расскажу о замыканиях для Java, которые, возможно, появятся уже в JDK 7. Скачать текущий прототип для экспериментов вот тут.

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

Так что же такое замыкание? Говоря неформально, это «вложенная» функция, которая содержит ссылки на свободные переменные (локальные переменные внешней функции). Замыкание создаётся каждый раз при выполнении её «внешней» функции. Мы можем выполнять нашу функцию, передавать в другие функции, а так же возвращать из других функций.

Анонимные классы как замыкания

В некотором своём виде замыкания, или лямбда-функции, в Java уже косвенно поддерживаются через внутренние анонимные классы. Это вполне объектно-ориентированное решение.

Вот как выглядит пример нечто похожего на замыкание с использованием анонимных классов:

interface I {
    void Block();
}                 

// ...                 

final String msg = "ADA";
new I() {
    public void Block() {
        System.out.println(msg);
    }
}.Block();

Замыкаем map и each с помощью анонимных классов

Возможно, вы помните пример с массивом из первой моей статьи. Мы напишем метод map, возвращающий новый массив, состоящий из элементов входного массива, к которым применена некая функция. Всё дело в том, что мы хотим указать эту функцию прямо в месте вызова map. Для этого нам нужно определить интерфейс, а позже реализовать его в нашем анонимном типе.

interface OneArgFunc<T> {
    T DoWork(T a);
}                 

// ...                 

public static <T> Iterable<T> map(Iterable<T> A, OneArgFunc<T> m) {
    ArrayList<T> newA = new ArrayList<T>();
    for (T pe : A)
        newA.add( m.DoWork(pe) );
    return newA;
}                 

// ...                 

Iterable<Integer> result = map(mylist,
    new OneArgFunc<Integer>() {
        public Integer DoWork(Integer a) {
            return a*a;
        }
    });

Достаточно много кода ради такой простой задачи. Помимо этого с анонимными типами мы имеем ещё несколько проблем. Например, мы не можем внутри нашего внутреннего класса изменять внешние переменные. Если мы хотим с ними работать, то мы должны объявить их как final. Так, если у нас есть метод each:

public static <T> void each(Iterable<T> A, OneArgFunc<T> m) {     for (T pe : A)         m.Proceed(pe);
}

Мы не можем сделать так:

boolean found = false;
each(mylist, new OneArgFunc<Integer>() {
        public void DoWork(Integer a) {
            if (a == 10)                found = true; // ошибка! flag должен быть final
        }
});

А если мы, например, захотим кинуть исключение из тела нашего «замыкания», то и тут нас подстерегают сложности и много излишнего кода. Компилятор может сгенерировать его автоматически, предоставив нам возможность записывать то, чего мы хотим добиться, другим, более элегантным способом. Плюс ко всему здесь значению ключевых слов this, break, continue и return придаётся не совсем не тот смысл, который мы можем ожидать от замыкания.

Замыкания как замыкания

Введение в язык «настоящих» замыканий призвано сократить количество кода, необходимого для выражения этой идеи. У нас появились новые «функциональные типы». Вот пример:

{int, int => int} sum = {int x, int y => x + y};
sum.invoke(2,3); // -> 5

Если нам необходимо будет кидать исключение из замыкания, то объявить это мы можем следующим образом:

{String => void throws IOException} print = ...

В конечном счёте, для этого генерируются интерфейсы, подобно тому, что мы рассмотрели в предыдущей главе.

Так как замыкания теперь у нас настоящие, мы можем изменять «внешние» переменные.

boolean myflag = false;
{ => void } block = { =>
	myflag = true;
	System.out.println("Hello from closure!");
};
System.out.println(myflag);
block.invoke();
System.out.println(myflag);

Даст следующий вывод:

false
Hello from closure!
true

Замыкаем map и each по-настоящему

Пользуясь новыми возможностями, перепишем нашу функцию map:

public static <T> Iterable<T> map(Iterable<T> a, {T => T} func) {
    ArrayList<T> b = new ArrayList<T>();
    for(T e : a)
        b.add( func.invoke(e) );
    return b;
}

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

Iterable<Integer> result = map(myintlist, {Integer e => e*e} );

Теперь на счёт функции each. Используя следующую версию:

public static <T> void each(Iterable<T> a, {T => void} func) {
    for (T e : a)
        func.invoke(e);
}

Мы можем проделать то, что в прошлый раз не получилось:

boolean found = false;
each(mylist, { int a =>
    if (a == 10) found = true; // всё ОК
});

Ещё пример

Допустим, нам нужно выполнить что-то в отдельном потоке. Мы можем поступить так:

final String message = "hello!";
new Thread(new Runnable() {     public void run() {
	System.out.print(message);
	System.out.printf("We in thread %d\n",              Thread.currentThread().getId());     }
}).start();

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

public static void async(final { => void } block) {
	new Thread( new Runnable() {
		public void run() {
			block.invoke();
		}
	}).start();
}

Так что теперь мы можем писать более лаконично:

String message = "hello!";
async({ =>     System.out.println(message);     System.out.printf("We in thread %d\n",
            Thread.currentThread().getId());
});

Либо вообще так (но у меня следующий вариант не скомпилировался):

String message = "hello!";
async {     System.out.println(message);     System.out.printf("We in thread %d\n",
            Thread.currentThread().getId());
}

Заключение

Думаю, введение замыканий в Java может быть хорошей идеей. Они могут помочь не только в работе с контейнерами и алгоритмами, но и других случаях. Однако мне, разбалованному фишками C# 3.0, кажется, что здесь не хватает ещё вывода типов, методов расширения и прочего «сахара». В документации по замыканиям рассказано о «Control invocation syntax», позволяющему ещё больше упростить синтаксис замыканий (например, не писать круглые скобки или => там где без этого можно обойтись), однако запустить эти примеры у меня не получилось. Видимо, всё это ещё в разработке. Тем не менее, радует, что разработка ведётся, и интересно будет проследить за тем, куда она заведёт :).

Дополнительная информация

Страница проекта – http://www.javac.info

Описание замыканий для Java – http://www.javac.info/closures-v05.html

Блог Zdeněk Troníček, посвящённый замыканиям в Java – http://tronicek.blogspot.com/

Пара видео о замыканиях в Java – тут и тут.

Functional Programming, google, html, Java

Получаем вывод дочернего процесса в Unix

Буквально вчера один человек задал мне вопрос, на который я не смог сходу ответить: как получить вывод дочернего процесса и записать его в строку? Другими словами, необходимо запустить какую-то внешнюю программу, и прочитать её вывод. Всё это должно быть реализовано программно, на Си. Судя по всему, решения могут варьироваться от одной операционной системы к другой, так, я немного повозился с данной задачей. Здесь я покажу, как это можно сделать в Unix при помощи каналов.

Анонимные каналы (anonymous pipes) – один из способов взаимодействия процессов. Мы можем записывать данные в канал на одном конце (в одном процессе), а считывать на другом конце (в другом процессе). Каналы – важная часть Unix-подобных операционных систем, они позволяют организовывать цепочки действий, когда вывод одной программы поступает на вход другой, и так далее.

В Unix shell анонимный канал создаётся при помощи символа прямого слеша |. Например:

$ ps -A | grep init
1 ?        00:00:01 init

Здесь стандартный вывод ps перенаправляется на стандартный ввод grep.

Вернёмся к нашей задаче. Нам нужно запустить дочерний процесс, который запустит внешнюю программу. Это мы проделаем при помощи fork(). По-умолчанию stdout дочернего процесса направлен на экран. Но нам нужно записать эти данные в строковой буфер, чтобы потом как-то с ними работать. Тут нам на помощь и приходят анонимные каналы. Мы создаём такой канал при помощи pipe(), а затем перенаправляем стандартный вывод дочернего процесса в ввод этого канала при помощи dup2(). После этого мы запускаем внешнюю программу при помощи execlp(), и весь её вывод пойдёт не на экран, а в наш канал.

В родительском же процессе мы читаем данные из канала в строковый буфер при помощи read().

Привожу исходный код демонстрационной программы:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>  

#define SIZE 256  

int main (int argc, char *argv[]) {
    int mypipe[2];
    pipe(mypipe);  

    switch(fork()) {
        case -1: /* error */
            perror(”fork”);
            exit(EXIT_FAILURE);
        case 0: /* child process */
            close(mypipe[0]); /* close unused in */
            dup2(mypipe[1], 1); /* stdout to pipe out */
            execlp(”ls”, NULL);
            close(mypipe[1]);
            _exit(EXIT_SUCCESS);
        }  

    /* parent process */
    close(mypipe[1]); /* close unused out */
    char buf[SIZE] = “”;
    read(mypipe[0], buf, SIZE); /* read from pipe in */
    close(mypipe[0]);
    printf(”Child output:\n%s\n”, buf);  

    return EXIT_SUCCESS;
}

Функция pipe(int filedes[2]) создаёт канал и записывает дескрипторы ввода и вывода соответственно в filedes[0] и filedes[1]. 0 – это ввод, 1 – это вывод. Мы используем знание этого при перенаправлении стандартного вывода дочернего процесса в вывод канала при помощи dup2(mypipe[1], 1). В каждом процессе мы сперва закрываем неиспользуемый «конец» канала, а затем работаем с оставшимся.

Стоит отметить, что в этом примере мы сможем получить только данные из стандартного вывода дочернего процесса. Стандартный вывод ошибок по-прежнему направлен на экран.

Вывод программы:

$ ./pipestest
Child output:
pipestest
pipestest.c

Исходный текст вы можете загрузить отсюда.

Подробную информацию о используемых функциях вы можете найти в manpages: fork(2), pipe(2), dup(2), exec(3) и т.д.

opera, Programming, Unix, аноним