Не так давно столкнулся по службе с весьма любопытной задачей. Но нет ничего нового под луной — и задача вам давно знакома: двойная диспетчеризация в C# в терминах статической типизации. Подробнее объясню ниже, но тем, кто и так всё понял, скажу: да, я буду применять «посетителя», но довольно необычно.

Ещё несколько оговорок, перед тем, как сформулировать задачу строже: я не буду останавливаться на том, почему мне не подходят dynamic, явная проверка типов через приведение и рефлексия. Тому две причины: 1) цель — избавиться от runtime исключений 2) хочу показать, что язык достаточно выразителен, даже если не прибегать к перечисленным средствам и оставаться в рамках строгой типизации.

Постановка


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

Пример:
  • скажем, есть некоторый интерфейс ICell, который выполняют классы RedCell, BlueCell, GreenCell.
  • есть правила следующего вида:
    1. получив две красных ячейки, пишем «красное на красном»
    2. получив зелёную и синюю пишем «берег»
    3. во всех прочих случаях — указать первый и второй цвет

  • прямым произведением множества ячеек на себя же получаем:

    красное на красном
    красный --> зелёный
    красный --> синий
    зелёный --> красный
    зелёный --> зелёный
    берег
    синий --> красный
    синий --> зелёный
    синий --> синий


Думаю, очевидно, что для решения такой задачи нам хватило бы одного класса вида:

internal class Cell
    {
        public Cell(string color)
        {
            Color = color;
        }

        public string Color { get; private set; }
    }

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

    internal interface ICell
    {
        //Some code
    }

    internal class RedCell : ICell
    {
        public string Color
        {
            get { return "красный"; }
        }
        //Some code
     }

Классический посетитель


Классикой уже стало решение такой задачи с одним элементом. Останавливаться на нём не стану, подробно об этом можно узнать, например, в Википедии. В нашем случае решение выглядело бы так следующим образом. При вот такой модели (обозначим этот код за [1], ниже вспомню о нём):

    internal interface ICell
    {
        T AcceptVisitor<T>(ICellVisitor<T> visitor);
    }

    internal interface ICellVisitor<out T>
    {
        T Visit(RedCell cell);

        T Visit(BlueCell cell);

        T Visit(GreenCell cell);
    }

    internal class RedCell : ICell
    {
        public string Color
        {
            get { return "красный"; }
        }

        public T AcceptVisitor<T>(ICellVisitor<T> visitor)
        {
            return visitor.Visit(this);
        }
    }

    internal class BlueCell : ICell
    {
        public string Color
        {
            get { return "синий"; }
        }

        public T AcceptVisitor<T>(ICellVisitor<T> visitor)
        {
            return visitor.Visit(this);
        }
    }

    internal class GreenCell : ICell
    {
        public string Color
        {
            get { return "зелёный"; }
        }

        public T AcceptVisitor<T>(ICellVisitor<T> visitor)
        {
            return visitor.Visit(this);
        }
    }

Простой посетитель легко решает нашу задачу:


internal class Visitor : ICellVisitor<string>
    {
        public string Visit(RedCell cell)
        {
            // здесь мы получаем доступ уже не к 
            // ICell, но к RedCell и к её не вынесенному
            // в интерфейс свойству Color
            return cell.Color;
        }

        public string Visit(BlueCell cell)
        {
            return cell.Color;
        }

        public string Visit(GreenCell cell)
        {
            return cell.Color;
        }
    }

Применение посетителя к задаче


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

    internal interface ICell
    {
        T AcceptVisitor<T>(ICellVisitor<T> visitor);
    }

    internal class RedCell<T> : ICell, ICellVisitor<T>
    {
        private readonly IProcessor<T> _processor;

        public RedCell(IProcessor<T> processor)
        {
            _processor = processor;
        }

        public TVisit AcceptVisitor<TVisit>(ICellVisitor<TVisit> visitor)
        {
            return visitor.Visit(this);
        }

        public string Color
        {
            get { return "red"; }
        }

        public T Visit(RedCell<T> cell)
        {
            return _processor.Get(this, cell);
        }

        public T Visit(BlueCell<T> cell)
        {
            return _processor.Get(this, cell);
        }

        public T Visit(GreenCell<T> cell)
        {
            return _processor.Get(this, cell);
        }
    }

    interface IProcessor<T>
    {
        T Get(RedCell<T> a, RedCell<T> b);
        T Get(RedCell<T> a, BlueCell<T> b);
        T Get(RedCell<T> a, GreenCell<T> b);
    }

Теперь всё, что нам остаётся — дописать простой процессор, и задача решена!

    internal class Processor : IProcessor<string>
    {
        public string Get(RedCell<string> a, RedCell<string> b)
        {
            return "красное на красном";
        }

        public string Get(RedCell<string> a, BlueCell<string> b)
        {
            return GeneralCase(a, b);
        }
        
        public string Get(RedCell<string> a, GreenCell<string> b)
        {
            return GeneralCase(a, b);
        }

        private string GeneralCase(ICell a, ICell b)
        {
            return a.Color + " --> " + b.Color;
        }
    }

Критика найденного решения


Что ж, действительно, решение найдено. Вам оно нравится? Мне — нет. Причины какие:
  • Нам необходимо написать N x N методов Visit: в каждой ячейке принять каждую; добавление ячейки нового цвета заставляет нас писать ещё N+1 новых методов. Если N достаточно велико (два десятка, например), то объём кода и трудозатрат устрашают;
  • Добавление любой новой ячейки необходимо приводит к изменению всех предыдущих. А если они в разных сборках, например? И, вполне вероятно, одну из этих сборок мы не может или не хотим менять;
  • В классы нашей модели мы добавили несвойственные им методы. Нужно пояснить — решить задачу, не добавив вовсе ни одного метода для посетителя невозможно (если вы можете — непременно мне расскажите!), но готовы ли мы терпеть N+1 дополнительный метод (в каждом классе!), не несущий никакой логической нагрузки? Увы, это синтаксический мусор;
  • Вы наверняка заметили, что ячейка стала обобщённой над T, именно:

    class RedCell<T> : ICell, ICellVisitor<T>
    

    Это уже совсем никуда не годится! Я не могу создать ячейку, не определившись заранее, какой тип будет возвращать заходящий в неё посетитель! Полнейший вздор.

    Разумеется, можно избавиться от обобщения: пусть посетитель ничего не возвращает, но изменяет своё состояние, которое выставит наружу открытым образом, но: (1) я предпочитаю immutable state и программирование, смещённое к функциональному (позвольте мне опустить мотивацию — думаю, она довольно понятна), значит, нужно избегать действий (Action) и стремиться использовать функции (Fun), значит, хорошо бы посетителю возвращать тип.

Надеюсь, перечисленного хватит, чтобы подтвердить моё мнение — решение так себе, однако, есть ещё одно важное замечание, на котором я хочу остановится подробнее. Задумаемся, сколько методов должен содержать IProcessor? N x N. То есть, очень много. Но ведь скорее всего нам нужно специальная обработка для очень небольшого, линейного по N, числа случаев. И тем не менее, мы не можем заранее знать, какие из них нам пригодятся (а мы ведь пишем framework, не правда ли? Структуру классов, основу, которыми все потом будут пользоваться, подключая нашу сборку к своим решениям).

Как его можно улучшить? Очевидный шаг: отделим модель от посетителя. Да, пусть, как и прежде, каждая ячейка умеет AcceptVisitor(...), но все методы Visit будут в отдельных классах. Несложно понять, что нам понадобится, в таком случае N+1 класс, каждый из которых содержит N методов Visit. Неслабо, правда? При этом любая новая ячейка приводит к добавлению нового класса + по методу в каждый из уже существующих.

Лучшее найденное решение


У меня есть решение, которое не обладает этими недостатками, а именно: мне нужно несколько классов (говорю без точности, потому что разные синтаксические красивости вроде fluent interface, от которых я не смог удержаться, прибавляют к этому числу, но использовать ли их — дело вкуса), причём число классов от N зависит; при добавлении новой ячейки мне понадобится добавить не зависящее от N число методов в эти классы.

Если (ну, мало ли) вы всё ещё читаете, то задумайтесь на мгновение, можете ли вы предложить решение, удовлетворяющее этим требованиям?

Если да, то здорово, напишите мне, но моё вот:

Модель у нас по-прежнему вида [1], а вот так (чтобы немного заинтриговать терпеливого читателя) будет выглядеть аналог конкретного процессора из предыдущего примера:

internal class ConcreteDispatcherFactory
    {
        public ICellVisitor<ICellVisitor<string>> CreateDispatcher()
        {
            return
                new PrototypeDispatcher<string>(Do)
                    .TakeRed.WithRed(Do)
                    .TakeGreen.WithBlue(Do);
        }

        private string Do(ICell a, ICell b)
        {
            var colorRetriever = new ColorRetriever();
            var aColor = a.AcceptVisitor(colorRetriever);
            var bColor = b.AcceptVisitor(colorRetriever);

            return aColor + "\t-->\t" + bColor;
        }

        private string Do(GreenCell a, BlueCell b)
        {
            return "побережье";
        }

        private string Do(RedCell a, RedCell b)
        {
            return "красное на красном";
        }
    }

где ColorRetriever — это простой «одинарный» визитёр:

internal class ColorRetriever : ICellVisitor<string>
    {
        public string Visit(RedCell cell)
        {
            return cell.Color;
        }

        public string Visit(BlueCell cell)
        {
            return cell.Color;
        }

        public string Visit(GreenCell cell)
        {
            return cell.Color;
        }
    }

[Перед, собственно, самим решением, небольшое отступлении об этом последнем ColorRetriever — хочу заострить читательское внимание на том, что сами по себе строчки
            var colorRetriever = new ColorRetriever();
            var aColor = a.AcceptVisitor(colorRetriever);
            var bColor = b.AcceptVisitor(colorRetriever);

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

Как видно, мы оговариваем общий случай и два частных, и этого будет достаточно, чтобы построить решение.
По большому счёту, нам понадобятся два класса — первый и второй посетитель. Второй будет обобщённым и первый создаст его типизированный экземпляр с тем, чтобы использовать для конкретной ячейки.

Вот первый:

class PrototypeDispatcher<TResult> : ICellVisitor<ICellVisitor<TResult>>
    {
        private readonly Builder<TResult, RedCell> _redBuilder;
        private readonly Builder<TResult, GreenCell> _greenBuilder;
        private readonly Builder<TResult, BlueCell> _blueBuilder;

        public PrototypeDispatcher(Func<ICell, ICell, TResult> generalCase)
        {
            _redBuilder = new Builder<TResult, RedCell>(this, generalCase);
            _blueBuilder = new Builder<TResult, BlueCell>(this, generalCase);
            _greenBuilder = new Builder<TResult, GreenCell>(this, generalCase);
        }

        public IBuilder<TResult, RedCell> TakeRed
        {
            get { return _redBuilder; }
        }

        public IBuilder<TResult, BlueCell> TakeBlue
        {
            get { return _blueBuilder; }
        }

        public IBuilder<TResult, GreenCell> TakeGreen
        {
            get { return _greenBuilder; }
        }

        public ICellVisitor<TResult> Visit(RedCell cell)
        {
            return _redBuilder.Take(cell);
        }

        public ICellVisitor<TResult> Visit(BlueCell cell)
        {
            return _blueBuilder.Take(cell);
        }

        public ICellVisitor<TResult> Visit(GreenCell cell)
        {
            return _greenBuilder.Take(cell);
        }
    }

Вот второй:

    internal class Builder<TResult, TA> : IBuilder<TResult, TA>, ICellVisitor<TResult> where TA : ICell
    {
        private Func<TA, RedCell, TResult> _takeRed;
        private Func<TA, BlueCell, TResult> _takeBlue;
        private Func<TA, GreenCell, TResult> _takeGreen;
        private readonly Func<ICell, ICell, TResult> _generalCase;

        private readonly PrototypeDispatcher<TResult> _dispatcher;
        private TA _target;

        public Builder(PrototypeDispatcher<TResult> dispatcher, Func<ICell, ICell, TResult> generalCase)
        {
            _dispatcher = dispatcher;
            _generalCase = generalCase;

            _takeRed = (a, b) => _generalCase(a, b);
            _takeBlue = (a, b) => _generalCase(a, b);
            _takeGreen = (a, b) => _generalCase(a, b);
        }

        public PrototypeDispatcher<TResult> WithRed(Func<TA, RedCell, TResult> toDo)
        {
            _takeRed = toDo;
            return _dispatcher;
        }

        public PrototypeDispatcher<TResult> WithBlue(Func<TA, BlueCell, TResult> toDo)
        {
            _takeBlue = toDo;
            return _dispatcher;
        }

        public PrototypeDispatcher<TResult> WithGreen(Func<TA, GreenCell, TResult> toDo)
        {
            _takeGreen = toDo;
            return _dispatcher;
        }

        public TResult Visit(RedCell cell)
        {
            return _takeRed(_target, cell);
        }

        public TResult Visit(BlueCell cell)
        {
            return _takeBlue(_target, cell);
        }

        public TResult Visit(GreenCell cell)
        {
            return _takeGreen(_target, cell);
        }

        public ICellVisitor<TResult> Take(TA a)
        {
            _target = a;
            return this;
        }
    }

И ещё интерфейс для красоты, чтобы отделить строителя от посетителя (которые в обоих классах сливаются, но зато синтаксис вызова красивый):

    internal interface IBuilder<TResult, out TA>
    {
        PrototypeDispatcher<TResult> WithRed(Func<TA, RedCell, TResult> toDo);
        PrototypeDispatcher<TResult> WithBlue(Func<TA, BlueCell, TResult> toDo);
        PrototypeDispatcher<TResult> WithGreen(Func<TA, GreenCell, TResult> toDo);
    }

В заключение хочу сослаться на серию статей «про волшебников и воинов», где тоже обсуждаются вопросы диспетчеризации в C#.

Комментарии (37)


  1. maxim_0_o
    28.05.2015 21:13

    На сколько я понял, мы условились, что в интерфейсе не будет свойства Color. Однако в блоке кода под заголовком «Применение посетителя к задаче» это свойство в интерфейсе есть. Я что-то не правильно понял?


    1. Johanan Автор
      28.05.2015 21:21

      Вы всё правильно поняли, Color в ICell действительно не нужен. Опечатку поправил.


      1. dougrinch
        28.05.2015 21:35
        +4

        А как же

        private string Do(ICell a, ICell b)
        {
            return a.Color + "\t-->\t" + b.Color;
        }
        
        ?

        Хотя в данной реализации и не уйти от свойства в интерфейсе. Разве что создавать метод Do на все варианты, благо апи позволяет. Действительно удобное получилось.


        1. Johanan Автор
          28.05.2015 21:45

          А! Точно. Внимательно читаете — спасибо. Сперва у меня как раз был цвет в интерфейсе, а добыть нужно было, скажем, целое число, которым обладала только красная ячейка. На идее решения эта оплошность никак не скажется, но нужно подумать, как лучше поправить статью.
          Что нам нужно в методе Do? Знать цвет первой и второй ячейки, причём обе им обладают независимо друг от друга. Значит, нам нужен простой («классический») визитёр, который цвет добудет.


  1. lasalas
    28.05.2015 22:04

    Зачем так много кода для такой тривиальной задачи?


    1. Johanan Автор
      28.05.2015 22:06

      А как иначе вы её предлагаете решать, если отмести варианты с динамическим приведением типов?


      1. lasalas
        28.05.2015 22:15
        +2

        Как-то так, например

        public static string Match(ICell t1, ICell t2) { return Patterns .Select(x => x(t1, t2)) .First(x => x != null) (); } private static readonly Func<ICell, ICell, Func<string>>[] Patterns = { (t1, t2) => Match<RedCell, RedCell>(t1, t2, () => "красное на красном"), (t1, t2) => Match<GreenCell, BlueCell>(t1, t2, () => "побережье"), (t1, t2) => (() => t1.Color + "\t-->\t" + t2.Color) }; private static Func<string> Match<T1, T2>(ICell t1, ICell t2, Func<string> f) where T1 : ICell where T2 : ICell { return t1 is T1 && t2 is T2 ? f : null; }


  1. lasalas
    28.05.2015 22:22
    +3

    Как-то так, например

    public static string Match(ICell t1, ICell t2)
    {
    return Patterns
    .Select(x => x(t1, t2))
    .First(x => x != null)
    ();
    }

    private static readonly Func<ICell, ICell, Func>[] Patterns =
    {
    (t1, t2) => Match<RedCell, RedCell>(t1, t2, () => «красное на красном»),
    (t1, t2) => Match<GreenCell, BlueCell>(t1, t2, () => «побережье»),
    (t1, t2) => (() => t1.Color + "\t-->\t" + t2.Color)
    };

    private static Func Match<T1, T2>(ICell t1, ICell t2, Func f)
    where T1: ICell
    where T2: ICell
    {
    return t1 is T1 && t2 is T2? f: null;
    }

    PS: Похоже не работает форматирование


    1. lasalas
      28.05.2015 22:29
      +3

      Можно сделать еще компактнее, но более грубо

      public static string Match(ICell a, ICell b)
      {
      return (Match<RedCell, RedCell>(a, b, () => «красное на красном»)
      ?? Match<GreenCell, BlueCell>(a, b, () => «побережье»)
      ?? (() => a.Color + "\t-->\t" + b.Color))
      ();
      }


      1. Johanan Автор
        28.05.2015 23:11
        +3

        Понял теперь вашу идею. Смотрите, вы всё равно не уходите от динамического приведения типов. Например, если я прошу вас вывести не «красное на красном», а что-нибудь из открытых свойств ячейки, вам всё равно придётся приводить тип. А если так, то наша беседа сводится к другому вопросу: что лучше — обычный посетитель или перебор типов через switch? Убийственного аргумента в пользу первого у меня нет. Но, например, если добавляем новый элемент, а использована динамическая типизация, уже куда сложнее найти все места, которые нужно обновить, чтобы поддержать его (предполагаем, что таких switch'ей у нас довольно много), легче что-то пропустить и получить потом runtime exception.
        В нашем случае, когда обработка по умолчанию задана исключений не будет, но мы получим какой-то общий объект вместо интересующего нас частного случая. Узнаем об этом тоже, скорее всего, в runtime.

        Проще всего, наверное, задачу вообще решить через dynamic, тогда не нужно даже делать явной проверки типов, а если мы укажем обработку по умолчанию, то, опять же, исключения не будет, но там свои сложности (см. например статью по ссылке, которую я приводил).

        Обобщая вышесказанное: 1) при явном приведении типов больше вероятности получить ошибку на этапе исполнения 2) мне было интересно решить задачу без явного приведения типов, что тоже немаловажно.


        1. lasalas
          29.05.2015 07:12
          +2

          1. Не совсем понятно, почему надо бояться приведения типов и откуда должны взяться runtime exceptions, раз типизация статически проверяется компилятором.
          2. То что я описал — тот же самый visitor, только в профиль и без макияжа.
          3. Можно слегка доработать Match<>(), чтобы стали доступны фичи конкретных типов:

          private static Func Match<T1, T2>(ICell t1, ICell t2, Func<T1, T2, string> f)
          where T1: ICell
          where T2: ICell
          {
          if (t1 is T1 && t2 is T2)
          return () => f((T1)t1, (T2)t2);
          return null;
          }


          (t1, t2) => Match<GreenCell, BlueCell>(t1, t2, (x,y) => «побережье: » + x.GreenFeature + ", " + y.BlueFeature),


        1. DrReiz
          29.05.2015 15:30
          +2

          Производительность! Производительность — основное преимущество двойной диспетчеризации по сравнению с pattern matching-ом.
          Двойная диспетчеризация — это два virtcall-а на один вызов. Паттерн матчинг — это N проверок типов на каждый вызов.


          1. lasalas
            29.05.2015 15:39

            Зависит от реализации. Но особой разницы быть не должно.


            1. DrReiz
              29.05.2015 17:07

              Какие компиляторы умеют разворачивать паттерн матчинг по двум произвольным параметрам в C*O(1) и с C близкой к 1?


              1. Lol4t0
                29.05.2015 17:36

                1. DrReiz
                  29.05.2015 17:50
                  +1

                  Цитата от туда «The internals usually have a pattern match compiler which turns an advanced pattern match into a simpler series of checks with the goal to minimize the number of checks».
                  Соответственно, это будет С*O(кол-во правил) с C меньше 1 (приблизительно: 0.3 — 0.7), а не O(1).


                  1. Lol4t0
                    29.05.2015 20:19

                    Ну, ортогонализация проверок все-таки позволяет сократить количество проверок в разы. Конечно, некоторое их колчество остается. Но дело в том, что в реальной жизни количество вариантов паттеронов не слишком велико. И хотя ассимптотически они будут медленнее двух виртуальных вызовов, не надо забывать, что virtual call является большим барьером оптимизации, чем сравнение.

                    Я бы устроил тестирование, но не могу придумать задачку под него


                    1. DrReiz
                      29.05.2015 20:40

                      В трансляторах (особенно оптимизирующих) очень много паттерн матчинга.


              1. lasalas
                29.05.2015 20:23

                Можно представить (и реализовать) компилятор с любой степенью оптимизации сопоставления: со сбалансированныеми деревьями, хэшами, кэшами…

                Но это скорей рассуждения из серии «может ли бог создать такой камень, который не сможет поднять?» :)

                Что-то мне подсказывает, что если список сопоставления очень велик и разнообразен, то, видимо, имеются серьезные проблемы в архитектуре и решать надо именно их


      1. Mrrl
        29.05.2015 06:52

        А если ввести список пар (Match — обработчик), который можно пополнять снаружи, то и код функции Match(ICell,ICell) при добавлении нового цвета менять не придётся. Правда, от операции as внутри обработчиков избавиться не удастся.


  1. Trueteller
    28.05.2015 23:16
    +2

    На мой взгляд, показать выразительность языка не очень удалось. На F# с pattern matching получилось бы гораздо выразительные.
    Обильное применение паттернов для решения задачи, которую можно было бы решить в несколько строк, свидетельствует скорее о нехватке языковых конструкций.
    Плюс нарушение SOLID принципов, дупликация кода…
    Возможно, это решение имело больше плюсов в вашей бизнес задаче из реальной жизни.
    Спасибо за статью!


    1. Johanan Автор
      28.05.2015 23:43

      Вам спасибо отзыв!
      Не было цели доказать, что C# — самый выразительный язык. На F# match действительно куда проще решил бы эту задачу, ну так там и наследования не приветствуется, а вместо него Union.
      Я хотел скорее показать, то такая задача на C# в принципе разрешима при условии, что мы не создаём (N + const) классов. Для меня, по крайней мере, это было не очевидно.

      Про SOLID вы что именно имеете в виду? Что я смешал строителя и посетителя, или что-то другое?


      1. Trueteller
        29.05.2015 00:01
        +1

        В основном, Open-closed. Если надо будет добавить класс YellowCell, придется менять все вспомогательные классы.


        1. Johanan Автор
          29.05.2015 00:08

          А, ну тут иначе не совладать с задачей. И я бы даже не сказал, что это так уж плохо. Да, мне придётся менять вспомогательные классы, но, меняя их, я хотя бы задумаюсь, какими именно правилами должны пользоваться все мои конкретные обработчики для жёлтой клетки.
          Раз речь зашла о F# — что будет если мы добавим новый тип в unit? Нам придётся обновить все использования его и указать, что делать функции в случае жёлтой клетки. И так ли это плохо?


          1. Trueteller
            29.05.2015 00:15

            Если новый тип не требует специального поведения, F# код или реализация «в лоб» (как у lasalas) не изменятся.
            Если требует специального поведения — изменится в одном месте, а не 3х связаных классах.


            1. Johanan Автор
              29.05.2015 18:54

              Тут мне остаётся вам поверить — я не так хорошо знаю F#. Или, может, вы пример приведёте? Любопытно разобраться.


  1. Lol4t0
    29.05.2015 00:06
    +7

    И ТУТ В ТРЕД ВРЫВАЕТСЯ HASKELL

    -- цвета (кто же рабоатет со строками)
    data Color = Red | Blue | Green deriving (Show)
    
    -- интерфес
    class Colored a where
    	getColor :: a -> Color
    
    --- проверка
    process :: Colored a => a -> a -> String
    process a b = process' (getColor a) (getColor b)
    
    process' Red Red = "Red on Red"
    process' Green Blue = "Coast"
    process' a b = show a ++ " --> " ++ show b
    
    
    data MyColor = MyRed | MyGreen | MyBlue deriving (Read)
    
    -- имплементация интерфейса
    instance Colored MyColor where
    	getColor MyRed = Red
    	getColor MyGreen = Green
    	getColor MyBlue = Blue
    
    
    main = interact $ unlines . map ((\[a, b] -> process (read a::MyColor) (read b)) . words) . lines
    


    Извините


    1. 0xd34df00d
      29.05.2015 13:30
      +1

      Ну так, визиторы — костыль вокруг отсутствующего паттерн-матчинга.

      // зачем-то захотелось избавиться от process' через {-# LANGUAGE ViewPatterns #-} и

      process (getColor -> Red) (getColor -> Red) = "Red on Red"
      process (getColor -> Green) (getColor -> Blue) = "Coast"
      process a b = show (getColor a) ++ " --> " ++ show (getColor b)
      


      Надо уже прекращать упарываться этим стилем.


    1. Johanan Автор
      29.05.2015 18:57

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


  1. michael_vostrikov
    29.05.2015 08:02

    Мне почему-то кажется, что наследовать красную ячейку от ячейки — это примерно то же самое, как наследовать квадрат от прямоугольника. Если не секрет, какая была реальная задача?


    1. Johanan Автор
      29.05.2015 18:59

      Было дерево, где элементы — узлы. Наружу выставлены потомки списком и только. А в значении своя логика, вот с ней и нужно было разбираться.


  1. middle
    29.05.2015 11:50
    +1

    А вот как это выглядит в языке с нормальной множественной диспетчеризацией. Common Lisp:

    (defclass icell ()
      ())
    
    (defclass red-cell (icell)
      ())
    
    (defclass blue-cell (icell)
      ())
    
    (defclass green-cell (icell)
      ())
    
    
    (defgeneric colors (arg1 arg2)
      )
    
    (defmethod colors ((a icell) (b icell))
      (format nil "~S --> ~S" a b))
    
    (defmethod colors ((a green-cell) (b blue-cell))
      "побережье")
    
    (defmethod colors ((a red-cell) (b red-cell))
      "красное на красном")
    


    1. Johanan Автор
      29.05.2015 19:03

      А если определить, кроме generic только два метода с такой сигнатурой (icell, red-cell) и (red-cell, icell), а потом вызвать с двумя красными, то какой вызовется метод?


      1. middle
        29.05.2015 19:45

        В стандартном способе топологической сортировки методов вызовется (red-cell, icell). Можно сделать свой, если надо по-другому.


  1. lasalas
    29.05.2015 13:36
    +1

    match(red, red, «красное на красном» ).
    match(green, blue, «побережье»).
    match(X,Y, S) :- concat([X,' --> ', Y], S).

    :- match(green, white, S), write(S).

    (Prolog)


    1. middle
      29.05.2015 19:51

      Провокационный вопрос: а можно сделать подкласс от атома red? ;) Насколько это гибко и расширяемо?


      1. lasalas
        29.05.2015 20:14
        +1

        Разумеется, Prolog (классический) не поддерживает ООП. Но извратиться всегда можно ;)

        expands(X, X).
        expands(dark_red, red).
        expands(light_green, green).


        match(X, Y, «красное на красном» ) :- expands(X, red), expands(Y, red).
        match(X, Y, «побережье» ) :- expands(X, green), expands(Y, blue).


        :- match(dark_red, red, S), write(S).