Я тут в очередной раз пытаюсь перечитать (но уже печатную) SICP. Вторая глава там: Построение абстракций с помощью данных. Действительно, чтобы мы делали, если бы нельзя было из примитивных типов строить что-то побольше. И мне там очень понравился один примерчик. Про его и хочу рассказать. Если выключить машину времени и вернуться в реальность без скобочек, то в мейнстримных сейчас в наших палестинах C#/Java обычно для этих целей мы используем классы. Книжный пример рассказывал о паре. В C# для этих целей есть класс Tuple (там правда не только пара, но нам сойдёт). Так вот, а давайте представим, что в C# не было бы специальной конструкции языка class (ну и struct заодно). Как же теперь нам сделать пару или любую другую сложную структуру данных из примитивов языка? Я уже в статье раньше рассуждал немного о философской проблеме ООП языков: кто должен быть this'ом. Так вот под катом пример на C# (переделанный из SICP) демонстрирующий ненужность this :)
Что нам нужно от такой конструкции как пара? Возможность её создать из двух примитивов (или других пар), получить ключ или значение. Так как мы договорились, что классов у нас якобы нет, то у нас будут три метода: create, key, value. Первый создаёт пару, а второй и третий из пары вынимают ключ и значение соответственно. Вся магия скрыта конечно же в методе create, вот он
public static Func<Operation, object> create(object key, object value)
{
return delegate(Operation operation)
{
if (operation == Operation.Key)
return key;
else if (operation == Operation.Value)
return value;
else
throw new Exception("WTF?!");
};
}
}
Этот метод вместо того, чтобы вернуть объект класса (которых как мы договорились у нас нет) возвращает анонимную функцию, которая может вернуть ключ или значение (Operation это, как нетрудно догадаться, простенький enum), которые были переданы в этот метод. Нам помогают две вещи: возможность возвращать функцию (и принимать конечно, но это позже) и возможность создавать функцию внутри функции, чтобы захватить, так сказать, переданные нам объекты.
Остальные две функции выглядят достаточно просто, они принимают эту функцию, которая представляет пару, и вызывают её с определённой операцией.
public static object key(Func<Operation, object> pair)
{
return pair(Operation.Key);
}
public static object value(Func<Operation, object> pair)
{
return pair(Operation.Value);
}
А вот так этим всем добром можно пользоваться :)
var pair = create(1, 'a');
Console.WriteLine(key(pair) + ":" + value(pair));
Не сомненно, в реальной жизни никто и нигде так не делает. Это не удобно, не гибко и много других 'не'. Но пример, мне кажется, замечательный!
Целиком эту пару строчек выложил на gist.
UPDATE: Классный пример на Ruby от DNNX
def cons(x,y)
->(m) { m.call(x, y) }
end
def car(z)
z.call ->(p, q) { p }
end
def cdr(z)
z.call ->(p, q) { q }
end
pair = cons(1, "a")
puts car(pair) #=> 1
puts cdr(pair) #=> a
SICP... *пережил прилив ностальгии, вспомнил видео*
ОтветитьУдалитьА вообще насчет "никто и нигде" это вы зря. В языках, где можно полноценно гулять тем или иным способом по своим же синтаксическим деревьям (IIRC это зовется умным словом Homoiconicity) - достаточно удобный прием.
Из реальных применений о которых я знаю рулят DSL. Потом берется синтаксическое дерево программы на этом DSL и из него генерится другая программа: либо на другом DSL в этом же языке, либо на чем-то внешнем - от кода на С++ до машинных инструкций.
Я знаю реальную разработку на Haskell, где клиенту предоставили DSL в виде ограниченного набора функций того же Haskell, на котором он формировал задачи (на самом деле, функции просто выглядят так же, но переопределены в библиотеке преобразований). И потом, подключая ту или иную библиотеку преобразования, товарищи получали на выходе готовые документы во всяких пдфах или хтмлях, либо функцию, которая рассчитывает определенные параметры данной задачи, либо еще что-то.
Еще видел вживую маньяка... впрочем, хоть блог он писать и не любит, основные идеи здесь все же есть: http://xhalt.blogspot.com/2008/06/simple-way-of-ast-transformation-using.html
То есть, что я хочу сказать. Если у вас есть функция pair - ее не обязательно выполнять. Ее можно использовать как данные для извлечения чего-либо еще. На этом же принципе построен LINQ. Или, более приземленный пример - способ в C# вытащить из Expression имя свойства (чтобы не писать его в виде строки, плохо поддающейся рефакторингу).
ЗЫ. По поводу вопроса о статической типизации шарпа...
ОтветитьУдалитьВы, если я правильно понял, хотите сделать функцию (результат работы create), которая умеет возвращать два разных типа в зависимости от значения параметра. Что, очевидно, в статически типизированном языке невозможно.
То есть, нам нужно вернуть нечто, что будет содержать объект принадлежащий типу ключа, и объект принадлежащий типу значений. То есть объект.
На самом деле, в SICP с LISP'ом совершенно то же самое: ведь результатом работы функции cons является пара. Объект пары. И два метода доступа: car и cdr. Рассматривайте это как
public class Pair {
internal Pair(TKey key, TValue value) { _key=key; _value=value; }
internal TKey _key;
internal TValue _value;
}
public static class Pair {
public Pair cons(TKey key, TValue value)
{ return new Pair(
key, value); }
public TKey car(Pair pair) { return pair._key; }
public TKey cdr(Pair pair) { return pair._value; }
}
Получится полный аналог, а type inference уберет необходимость явно задавать какие бы то ни было типы.
Ну кроме того, что наши функции придется дергать через Pair.cons, Pair.car, Pair.cdr вместо более простого синтаксиса в LISP.
Ох уж этот умный форматтер... заставлять людей руками писать < и > негуманно :)
ОтветитьУдалитьУх... Сразу скажу спасибо за все комментарии!
ОтветитьУдалитьИменно в таком костыльном виде как я показал смысла никакого нет, поэтому так никто не делает. В List'ах можно использовать стандартную пару, в c# соответственно классы.
Да, edit у disqus так себе, но всё ж лучше стандартных комментариев от blogger'а :)
ОтветитьУдалитьНу т.е. я не против использования в с# делегатов и т.п. :) Я просто хотел сказать, что использовать их вот так, как замена другим более удобным вещам, которые доступны в языке, например создание классов, смысла особого нет. Или я вас не понял? Можно более подробно последний абзац? Просто я с внутренней кухней LINQ, честно говоря не очень хорошо знаком.
ОтветитьУдалитьДа не, я вполне понимаю шарп, спасибо)))
ОтветитьУдалитьПросто у тебя (надоело выкать, если напрягает - скажешь) у функции dispatch тип иначе как object не определить - я об этом говорил. Потому что между типом ключа и типом значения других общих предков может не оказаться.
:)
ОтветитьУдалитьДа, поэтому и использовал object. Просто тот код выше с class'ом уже не то, это стандартный ООП подход и методы car/cdr должны быть в самом классе, вообщем получается Tuple. Идея была обойтись без инструкции class, хотя бы внешне.
Но dispatch в этом случае да, может только самым верхнем object'ом оперировать.
В самом простом виде - LINQ to Objects - LINQ представляет собой просто удобно выбранный Fluent interface из Extension-методов. Там ничего интересного нет, в целом. Все внутренности очень подробно объяснил Jon Skeet в своем блоге, а потом собрал все 45 постов в одну ебуку: http://msmvps.com/blogs/jon_skeet/archive/2011/03/18/edulinq-the-e-book.aspx
ОтветитьУдалитьА вот LINQ to SQL и другие более хитрые версии - это уже гораздо-гораздо больше и интереснее. LINQ to SQL при вызове цепочки методов (которая внешне практически не отличается от LINQ to Objects) ничего не выполняет, а собирает Expression Trees. И когда ее попросят выполниться - она по этим Expression Tree прогуляется и преобразует их в SQL-запрос, который и будет отправлен серверу. То есть, по сути, LINQ выступает в роли этого самого DSL, по синтаксическому дереву которого мы гуляем и из него генерим совсем другую штуку - в данном случае, эквивалентный SQL запрос.
Подробно можно почитать в этой серии: http://blogs.msdn.com/b/mattwar/archive/2008/11/18/linq-links.aspx
Хотя в ней всего 17 постов - но чтиво посложнее Скита, за обедом не осилить :)
ЗЫ. Я этот твой коммент заметил чисто случайно... ну хоть бы какое-то email-оповещение настроить можно, когда на мой комментарий кто-то отвечает?
Хм.. интересно, надо будет на выходных почитать, спасибо!
ОтветитьУдалитьНа http://disqus.com/ можно настроить свой профиль, чтобы приходили нотификации с ответами на комментарий.
ОтветитьУдалитьЛибо в самом низу комментариев, можно просто на все подписаться по email или rss.
О, сенкс. Снизу я видел - но на каждый тред отдельно подписываться мне лениво. Настроил на disqus, поглядим-с :)
ОтветитьУдалитьТо же самое на руби: https://gist.github.com/992986
ОтветитьУдалитьНе знаю, зачем написал это. Просто пост перечитывал и решил попробовать - красиво получится или нет.
Чёто результат мне не очень нравится :)
А чего не нравиться?
ОтветитьУдалитьПо моему красивше чем на C#. Но с лиспом конечно никак тягаться не может :D
Да неканоничный код какой-то. Не руби-вэй абсолютно.
ОтветитьУдалитьТаки я так в конце и написал, что сама идея не очень из мира C#/Java и пр. ООПе :)
ОтветитьУдалитьЯ всё же не терял надежду. Всё-таки в рубях много из функционального мира.
ОтветитьУдалитьТам слишком мало скобочек ;)
ОтветитьУдалитьОлоло, это в хацкеле слишком мало скобочек.
ОтветитьУдалитьА они там вообще есть? :)
ОтветитьУдалитьААА, я нашёл! Блин, руби жжот однозначно! Держи: https://gist.github.com/993019
ОтветитьУдалитьОфигительно, вынес в сам пост!
ОтветитьУдалитьЕсли так много не, то зачем это все?
ОтветитьУдалитьРазминка для ума :)
ОтветитьУдалитьЧтобы лучше понять идею Лиспа: нет никакой разницы между данными и кодом :)