Ещё несколько оговорок, перед тем, как сформулировать задачу строже: я не буду останавливаться на том, почему мне не подходят dynamic, явная проверка типов через приведение и рефлексия. Тому две причины: 1) цель — избавиться от runtime исключений 2) хочу показать, что язык достаточно выразителен, даже если не прибегать к перечисленным средствам и оставаться в рамках строгой типизации.
Постановка
Иначе говоря, предположим, что у нас на руках есть некоторая коллекция разнотипных объектов с довольно общим и малосодержательным интерфейсом. Взяв любые два их них мы хотим понять тип каждого и обработать специальным методом.
Пример:
- скажем, есть некоторый интерфейс ICell, который выполняют классы RedCell, BlueCell, GreenCell.
- есть правила следующего вида:
- получив две красных ячейки, пишем «красное на красном»
- получив зелёную и синюю пишем «берег»
- во всех прочих случаях — указать первый и второй цвет
- прямым произведением множества ячеек на себя же получаем:
красное на красном
красный --> зелёный
красный --> синий
зелёный --> красный
зелёный --> зелёный
берег
синий --> красный
синий --> зелёный
синий --> синий
Думаю, очевидно, что для решения такой задачи нам хватило бы одного класса вида:
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)
lasalas
28.05.2015 22:04Зачем так много кода для такой тривиальной задачи?
Johanan Автор
28.05.2015 22:06А как иначе вы её предлагаете решать, если отмести варианты с динамическим приведением типов?
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; }
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: Похоже не работает форматирование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))
();
}Johanan Автор
28.05.2015 23:11+3Понял теперь вашу идею. Смотрите, вы всё равно не уходите от динамического приведения типов. Например, если я прошу вас вывести не «красное на красном», а что-нибудь из открытых свойств ячейки, вам всё равно придётся приводить тип. А если так, то наша беседа сводится к другому вопросу: что лучше — обычный посетитель или перебор типов через switch? Убийственного аргумента в пользу первого у меня нет. Но, например, если добавляем новый элемент, а использована динамическая типизация, уже куда сложнее найти все места, которые нужно обновить, чтобы поддержать его (предполагаем, что таких switch'ей у нас довольно много), легче что-то пропустить и получить потом runtime exception.
В нашем случае, когда обработка по умолчанию задана исключений не будет, но мы получим какой-то общий объект вместо интересующего нас частного случая. Узнаем об этом тоже, скорее всего, в runtime.
Проще всего, наверное, задачу вообще решить через dynamic, тогда не нужно даже делать явной проверки типов, а если мы укажем обработку по умолчанию, то, опять же, исключения не будет, но там свои сложности (см. например статью по ссылке, которую я приводил).
Обобщая вышесказанное: 1) при явном приведении типов больше вероятности получить ошибку на этапе исполнения 2) мне было интересно решить задачу без явного приведения типов, что тоже немаловажно.lasalas
29.05.2015 07:12+21. Не совсем понятно, почему надо бояться приведения типов и откуда должны взяться 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),
…
DrReiz
29.05.2015 15:30+2Производительность! Производительность — основное преимущество двойной диспетчеризации по сравнению с pattern matching-ом.
Двойная диспетчеризация — это два virtcall-а на один вызов. Паттерн матчинг — это N проверок типов на каждый вызов.lasalas
29.05.2015 15:39Зависит от реализации. Но особой разницы быть не должно.
DrReiz
29.05.2015 17:07Какие компиляторы умеют разворачивать паттерн матчинг по двум произвольным параметрам в C*O(1) и с C близкой к 1?
Lol4t0
29.05.2015 17:36DrReiz
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).Lol4t0
29.05.2015 20:19Ну, ортогонализация проверок все-таки позволяет сократить количество проверок в разы. Конечно, некоторое их колчество остается. Но дело в том, что в реальной жизни количество вариантов паттеронов не слишком велико. И хотя ассимптотически они будут медленнее двух виртуальных вызовов, не надо забывать, что virtual call является большим барьером оптимизации, чем сравнение.
Я бы устроил тестирование, но не могу придумать задачку под него
lasalas
29.05.2015 20:23Можно представить (и реализовать) компилятор с любой степенью оптимизации сопоставления: со сбалансированныеми деревьями, хэшами, кэшами…
Но это скорей рассуждения из серии «может ли бог создать такой камень, который не сможет поднять?» :)
Что-то мне подсказывает, что если список сопоставления очень велик и разнообразен, то, видимо, имеются серьезные проблемы в архитектуре и решать надо именно их
Mrrl
29.05.2015 06:52А если ввести список пар (Match — обработчик), который можно пополнять снаружи, то и код функции Match(ICell,ICell) при добавлении нового цвета менять не придётся. Правда, от операции as внутри обработчиков избавиться не удастся.
Trueteller
28.05.2015 23:16+2На мой взгляд, показать выразительность языка не очень удалось. На F# с pattern matching получилось бы гораздо выразительные.
Обильное применение паттернов для решения задачи, которую можно было бы решить в несколько строк, свидетельствует скорее о нехватке языковых конструкций.
Плюс нарушение SOLID принципов, дупликация кода…
Возможно, это решение имело больше плюсов в вашей бизнес задаче из реальной жизни.
Спасибо за статью!Johanan Автор
28.05.2015 23:43Вам спасибо отзыв!
Не было цели доказать, что C# — самый выразительный язык. На F# match действительно куда проще решил бы эту задачу, ну так там и наследования не приветствуется, а вместо него Union.
Я хотел скорее показать, то такая задача на C# в принципе разрешима при условии, что мы не создаём (N + const) классов. Для меня, по крайней мере, это было не очевидно.
Про SOLID вы что именно имеете в виду? Что я смешал строителя и посетителя, или что-то другое?Trueteller
29.05.2015 00:01+1В основном, Open-closed. Если надо будет добавить класс YellowCell, придется менять все вспомогательные классы.
Johanan Автор
29.05.2015 00:08А, ну тут иначе не совладать с задачей. И я бы даже не сказал, что это так уж плохо. Да, мне придётся менять вспомогательные классы, но, меняя их, я хотя бы задумаюсь, какими именно правилами должны пользоваться все мои конкретные обработчики для жёлтой клетки.
Раз речь зашла о F# — что будет если мы добавим новый тип в unit? Нам придётся обновить все использования его и указать, что делать функции в случае жёлтой клетки. И так ли это плохо?Trueteller
29.05.2015 00:15Если новый тип не требует специального поведения, F# код или реализация «в лоб» (как у lasalas) не изменятся.
Если требует специального поведения — изменится в одном месте, а не 3х связаных классах.Johanan Автор
29.05.2015 18:54Тут мне остаётся вам поверить — я не так хорошо знаю F#. Или, может, вы пример приведёте? Любопытно разобраться.
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
Извините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)
Надо уже прекращать упарываться этим стилем.
Johanan Автор
29.05.2015 18:57Ну, да. Кто бы спорил, что функциональные языке с такой задачкой на раз справляются. Но за пример спасибо, хороший!
michael_vostrikov
29.05.2015 08:02Мне почему-то кажется, что наследовать красную ячейку от ячейки — это примерно то же самое, как наследовать квадрат от прямоугольника. Если не секрет, какая была реальная задача?
Johanan Автор
29.05.2015 18:59Было дерево, где элементы — узлы. Наружу выставлены потомки списком и только. А в значении своя логика, вот с ней и нужно было разбираться.
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)) "красное на красном")
Johanan Автор
29.05.2015 19:03А если определить, кроме generic только два метода с такой сигнатурой (icell, red-cell) и (red-cell, icell), а потом вызвать с двумя красными, то какой вызовется метод?
middle
29.05.2015 19:45В стандартном способе топологической сортировки методов вызовется (red-cell, icell). Можно сделать свой, если надо по-другому.
lasalas
29.05.2015 13:36+1match(red, red, «красное на красном» ).
match(green, blue, «побережье»).
match(X,Y, S) :- concat([X,' --> ', Y], S).
:- match(green, white, S), write(S).
(Prolog)middle
29.05.2015 19:51Провокационный вопрос: а можно сделать подкласс от атома red? ;) Насколько это гибко и расширяемо?
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).
maxim_0_o
На сколько я понял, мы условились, что в интерфейсе не будет свойства Color. Однако в блоке кода под заголовком «Применение посетителя к задаче» это свойство в интерфейсе есть. Я что-то не правильно понял?
Johanan Автор
Вы всё правильно поняли, Color в ICell действительно не нужен. Опечатку поправил.
dougrinch
А как же
?Хотя в данной реализации и не уйти от свойства в интерфейсе. Разве что создавать метод Do на все варианты, благо апи позволяет. Действительно удобное получилось.
Johanan Автор
А! Точно. Внимательно читаете — спасибо. Сперва у меня как раз был цвет в интерфейсе, а добыть нужно было, скажем, целое число, которым обладала только красная ячейка. На идее решения эта оплошность никак не скажется, но нужно подумать, как лучше поправить статью.
Что нам нужно в методе Do? Знать цвет первой и второй ячейки, причём обе им обладают независимо друг от друга. Значит, нам нужен простой («классический») визитёр, который цвет добудет.