суббота, 14 мая 2011 г.

Мир вверх тормашками или код как данные

Я тут в очередной раз пытаюсь перечитать (но уже печатную) 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), которые были переданы в этот метод. Нам помогают две вещи: возможность возвращать функцию (и принимать конечно, но это позже) и возможность создавать функцию внутри функции, чтобы захватить, так сказать, переданные нам объекты.

  • Да я знаю, что компилятор превращает делегат в класс, что поделать, это ООП, но всё же пример демонстрирует то что я хотел на популярном языке без скобочек :) все остальные могут найти этот пример во второй главе SICP или переписать на любимом функциональном языке с настоящими функциями высших порядков
  • C# имеет статическую типизацию, поэтому пример c object'ом в качестве ключа/значения выглядит более коряво нежели оригинальный пример, может у кого будут идеи как это обойти?

Остальные две функции выглядят достаточно просто, они принимают эту функцию, которая представляет пару, и вызывают её с определённой операцией.


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

24 комментария:

  1. 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 имя свойства (чтобы не писать его в виде строки, плохо поддающейся рефакторингу).

    ОтветитьУдалить
  2. ЗЫ. По поводу вопроса о статической типизации шарпа...


    Вы, если я правильно понял, хотите сделать функцию (результат работы 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.

    ОтветитьУдалить
  3. Ох уж этот умный форматтер... заставлять людей руками писать &lt; и &gt; негуманно :)

    ОтветитьУдалить
  4. Ух... Сразу скажу спасибо за все комментарии!
    Именно в таком костыльном виде как я показал смысла никакого нет, поэтому так никто не делает. В List'ах можно использовать стандартную пару, в c# соответственно классы.

    ОтветитьУдалить
  5. Да, edit у disqus так себе, но всё ж лучше стандартных комментариев от blogger'а :)

    ОтветитьУдалить
  6. Ну т.е. я не против использования в с# делегатов и т.п. :) Я просто хотел сказать, что использовать их вот так, как замена другим более удобным вещам, которые доступны в языке, например создание классов, смысла особого нет. Или я вас не понял? Можно более подробно последний абзац? Просто я с внутренней кухней LINQ, честно говоря не очень хорошо знаком.

    ОтветитьУдалить
  7. Да не, я вполне понимаю шарп, спасибо)))

    Просто у тебя (надоело выкать, если напрягает - скажешь) у функции dispatch тип иначе как object не определить - я об этом говорил. Потому что между типом ключа и типом значения других общих предков может не оказаться.

    ОтветитьУдалить
  8. :)
    Да, поэтому и использовал object. Просто тот код выше с class'ом уже не то, это стандартный ООП подход и методы car/cdr должны быть в самом классе, вообщем получается Tuple. Идея была обойтись без инструкции class, хотя бы внешне.

    Но dispatch в этом случае да, может только самым верхнем object'ом оперировать.

    ОтветитьУдалить
  9.  В самом простом виде - 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-оповещение настроить можно, когда на мой комментарий кто-то отвечает?

    ОтветитьУдалить
  10. Хм.. интересно, надо будет на выходных почитать, спасибо!

    ОтветитьУдалить
  11.  На http://disqus.com/ можно настроить свой профиль, чтобы приходили нотификации с ответами на комментарий.

    Либо в самом низу комментариев, можно просто на все подписаться по email или rss.

    ОтветитьУдалить
  12.  О, сенкс. Снизу я видел - но на каждый тред отдельно подписываться мне лениво. Настроил на disqus, поглядим-с :)

    ОтветитьУдалить
  13. То же самое на руби: https://gist.github.com/992986
    Не знаю, зачем написал это. Просто пост перечитывал и решил попробовать - красиво получится или нет.
    Чёто результат мне не очень нравится :)

    ОтветитьУдалить
  14. А чего не нравиться?

    По моему красивше чем на C#. Но с лиспом конечно никак тягаться не может :D

    ОтветитьУдалить
  15. Да неканоничный код какой-то. Не руби-вэй абсолютно.

    ОтветитьУдалить
  16. Таки я так в конце и написал, что сама идея не очень из мира C#/Java и пр. ООПе :)

    ОтветитьУдалить
  17. Я всё же не терял надежду. Всё-таки в рубях много из функционального мира.

    ОтветитьУдалить
  18. Ололо, это в хацкеле слишком мало скобочек.

    ОтветитьУдалить
  19. ААА, я нашёл! Блин, руби жжот однозначно! Держи: https://gist.github.com/993019 

    ОтветитьУдалить
  20. Офигительно, вынес в сам пост!

    ОтветитьУдалить
  21. Если так много не, то зачем это все?

    ОтветитьУдалить
  22. Разминка для ума :)
    Чтобы лучше понять идею Лиспа: нет никакой разницы между данными и кодом :)

    ОтветитьУдалить