Pull to refresh

Comments 34

Если правильно понял суть статьи, в ООП языках примеси (mixin) являют собой очень похожую концепцию
Похожую, только требующую явного использования. Я пытаюсь это сделать более автоматизировано.
Если разработчик наследует кенгуру от пехотинца, то даже не знаю, что тут может помочь.
Мне кажется, что я знаю — хочу проверить. :-)
Жуткий синтаксис, куча странных повторений. По моему мощь лиспа в том, чтобы не писать на птичьем языке, а в возможности определить свой DSL и с его помощью уже описать то, что нужно, максимально лаконично.
У меня тут есть идея, сделать «двумерный» диалект лиспа.

Например, вот этот код:
(defmethod canFireball (get-grid-node {:magic :fire, :limbs :hands} #'grid) [p] (> (:mana p) 0))
; владеющие магией огня и имеющие рука - пускают, если есть мана.


Мог бы выглядеть куда более дружелюбно:
!func can
	doc =владеющие магией огня и имеющие рука - пускают, если есть мана.
	>
		action fireball
		obj
			magic fire
			limbs hands
			mana int
		power int
	< is-desc
		obj/mana
		power


И пример использования:
!obj alice
	descr =Алиса - это слабенький такой маг огня
	magic fire
	limbs hands
	mana int =10

# выводим в консоль может ли Алиса кинуть минимальный огненный шар
< can
	action fireball
	obj alice
	power int =5


Мне кажется, избавление от бесконечных скобочек и прочей пунктуации в пользу говорящих имён, хоть и сделает код слегка длиннее, но он будет легче восприниматься, особенно новичками. И тогда идеи лиспа смогут найти место в головах у широкой аудитории. Что вы думаете на этот счёт?
Не уверен, что ваш вариант выглядит более дружелюбно.
А я вот, как человек, специализирующихся на более популярных языках, вижу исходный вариант, как какое-то заклинание, которое совершенно не понятно что делает. DSL всё же должен быть ближе к предметной области.
Уточню: мне на самом деле не нравится ни оригинал, ни ваша замена для него :) Но если выбирать из двух зол, то…
Синтаксис — дело вкуса. Честно говоря я к синтаксису более-менее равнодушен, лишь бы он был не слишком многословным и не слишком сложным. Но если будут идеи, удобные разработчикам — попробую их реализовать.
А в чем Вы видите повторения? Результат get-grid-node можно сохранить в переменную с понятным именем и повторно использовать.
Постоянно повторяется мантра «get-grid-node {} #'grid».
Это фактически имя типа. Можно ему создать синоним
(def FireHands (get-grid-node {:magic :fire, :limbs :hands} #'grid))

(defmethod canFireball FireHands [p] (> (:mana p) 0))

(def simpleMage (withMeta {:grid-hierarchy-cache FireHands} {:mana 100}))
Ничего не поменялось, только кода стало больше…
Следующий шаг — написать макрос, создающий тип и функцию-конструктор:
(defmacro def-grid-node [name props grid] ...)

(def-grid-node FireHands {:magic :fire, :limbs :hands} #'grid)

(defmethod canFireball FireHands [p] (> (:mana p) 0))

(def simpleMage (->FireHands {:mana 100}))

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

Возможно ли всего этого избежать?

Тут вы ставите правильный вопрос. Действительно, если у нас что-то сложное, то может мы ошиблись на каком-то шаге и придумали нежизнеспособный механизм? Вы предлагаете сделать шаг по улучшению этого механизма.
С моей точки зрения естественное затруднение здесь в головах, а именно в желании сделать доступным все методы «через одну точку». Это желание пихать разные обязанности в один объект приводит к трудностям, которые вы описали. Это же желание погубит и описанный вами метод, хотя он, безусловно, может иметь какие-то преимущества в читаемости. Я считаю тут нужно сделать не один шаг, а два, а именно отказаться от наследования в пользу композиции. Разбивать объект на подсистемы. Тогда пересечение между разнотипными интерфейсами в рамках одного объекта просто исчезнет как явление.
Эта идея родилась из обсуждения одного проекта, сделанного на CMS (не язык программирования) с наследованием (оформление и контент странички сайта наследовались). Структура разделов сайта была двумерной, и я решил задачу используя множественное наследование. Мой оппонент хотел избавится от множественного наследования, но у нас не получилось. Тогда он предложил эту схему. Правда она так и не была реализована.
Потом я попытался перенести подход в область программирования. Не думаю, что это панацея. Но мне кажется, что этот метод будет удачным для систем с большим числом классов с тонкими отличиями (как типы юнитов в играх) или в библиотеках, позволяющих делать тонкую настройку (выбирать способ управления памятью, управления конкуреннтностью и тп). В этих задачах можно использовать миксины, но они могут сделать иерархию запутанной.
А Принцип Подстановки Лисков ни кто не отменял. И, по моему, следовать ему здесь будет удобнее, чем при классическом явном наследовании.
Насколько я понял, идея заключается в диспетчеризации методов по набору мета-свойств объекта. Каждый объект помимо своих свойств получает мета-свойства «класса»: феникс из примера не просто некое существо с заданной силой и маной, это магическое существо с крыльями, владеющее магией огня.

         раса     магия   конечности  материал  есть ручка для переноски?
маг      маг      огонь   руки        -         -
варвар   человек  -       руки        -         -
феникс   маг      огонь   крылья      -         -
эльф     эльф     воздух  руки        -         -
чайник   -        -       -           железо    да
тарелка  -        -       -           фарфор    нет
голем    голем    -       руки        железо    -


Поиск метода происходит по набору мета-свойств объекта, сам метод выполняется над свойствами объекта. Невозможно выполнить метод над объектом, который не предназначен для этого.
Эльф не расплавится при температуре 1540 °C как чайник (и голем). Железный голем не может летать, но может развести огонь. У кенгуру нет рук, он не сможет стрелять из ПЗРК.

Преимущество представленного метода по сравнению с миксинами или какой-либо формой множественного наследования заключается в декларативном описании мета-свойств объектов и в безопасном выполнении любой функции над любым объектом (при условии адекватного ничего-не-делающего поведения по-умолчанию).
Вопрос, как обрабатываются различные действия у различных классов? Например, у мага-человека на файрбол уйдет 10 маны, а у мага-эльфа — 20?
Сделать специализированный по расе метод, который возвращает стоимость заклинания.
(defmulti fireballCost (grid-dispatch1) :hierarchy #'grid)

(defmethod fireballCost (get-grid-node {:race :human} #'grid) [p] 10)
(defmethod fireballCost (get-grid-node {:race :elf} #'grid) [p] 20)
Возможно ли всего этого избежать?
Да, и вы на самом деле движетесь в правильную сторону. Но деле в том, что это всё уже сделано за нас. Computer Science, благо, не стоит на месте. Современные языках программирования (Rust, Haskell, Swift, Scala и пр.) предоставляют более элегантные средства полиморфизма, чем традиционное для ООП наследование (которое вобще анти-паттерн и заноза в заднице). Рекомендую ознакомится с концепцией тайпклассов.
Мне многие уже немекали на сходство этого наследования и классами типов. Я вынужден был задуматься наж этим.
Но пришел к выводу, что это разные вещи и одно другого не заменил.
Классическое наследование обычно проще, чем классы типов и помогает повторному использованию, позволяя расширять (и иногда сцеплять) существующие типы данных. Классы типов позволяют объединять под общим интерфейсом несвязанные между собой типы данных. Одно другого на мой взгляд полностью не заменяет.
На классах типов можно реализовать некоторые подходы классического ООП, но порождение реазизаций класса типа на основе интерфейсов других классов типов, а не структур данных (аналог наследования), все равно порождает жесткую иерархию, встроится в середину которой будет проблематично.
(defmethod canFire (get-grid-node {} #'grid) [p] false)
; огнем, поумолчанию, ни кто не владее
(defmethod canFire (get-grid-node {:limbs :hands} #'grid) [p] true)
; рукастые могут развести огонь
(defmethod canFire (get-grid-node {:magic :fire} #'grid) [p] (> (:mana p) 0))
; владея магией руки иметь не обязательно
(defmethod canFire (get-grid-node {:magic :fire, :limbs :hands} #'grid) [p] true)
; магия и руки совместимы - Clojure боится перепутать причину, по которой дано это свойства


Коммент под строкой?
За что вы так со мной?
Такое наследование я уже пробовал реализовать на Common Lisp. Но устройство MOP я не знаю, и та реализация не встроена в CLOS и не слишком эффективна.
А вам не диспетчеризация по предикату нужна? Если её прикрутить в CLOS, то там можно выразить какие угодно условия для применения методов.
На основе аналога 'isa?'? Не обязательно. Достаточно разобраться как менять предков у ранее описанных классов.
Нет. Чтобы на методы обобщённых функций можно было навешивать дополнительные условия применимости. Для каждого вызова фактически применяться будут только те методы, для которых выполяются установленные для них условия.

Что-то вроде:
(defpmethod can-fire? ((char <character>))
  (:when (eq 'hands (limbs char)))
  t )
Этот метод can-fire? будет вызываться, если ей передадут экземпляр класса <character>, у которого в слоте limbs лежит значение hands. Иначе этот код вообще не будет выполнен и управление пойдёт к следующему потенциально применимому методу.

Мне кажется, такой подход выражает вашу идею на более общем уровне.
А если предикат не выполняется — переходить к суперклассу?
К следующему методу; как будто бы этот метод просто сделал (call-next-method). CLOS допускает, что несколько методов могут быть применимы к набору аргументов. Все они упорядочиваются по некоторым правилам и комбинируются в эффективный метод.

Например, стандартные методы упорядочиваются в соответствии с иерархией наследования классов аргументов (методы суперклассов выполняются после методов подклассов) и могут иметь один из квалификаторов:
  • around: выполняются первыми; должны вызвать (call-next-method), чтобы выполнить все остальные методы;
  • before: выполняются после around; все before-методы выполняются в поряке убывания конкретности классов аргументов;
  • primary: квалификатор по умолчанию; выполняется только один primary-метод, для самого конкретного набора аргументов, но он может вызвать (call-next-method), чтобы выполнить следующий менее конкретный метод (для суперклассов аргументов);
  • after: выполняются после primary; все after-методы выполняются в поряке возрастания конкретности классов аргументов;
В цепочке должен быть как минимум один primary-метод, остальные опциональны.

В вашем случае будет то же самое. Если не выполняется какой-то предикат, то переходим к следующему методу: это будет или метод такого же уровня конкретности, но с другим предикатом, или же метод для суперклассов, если методы одного уровня закончились. Плюс, по идее, необходимо дополнительное условие, что хотя бы для одного primary-метода предикат возвращает истину.
UFO just landed and posted this here
> Наследование, при кажущейся простоте, часто приводит к сложным, сопротивляющимся изменениям структурам.

; поумолчанию персонажи не летают

Скажите, сколько кода надо перелопатить, чтобы инвертировать поведение? Вы проблему то не решили.

Мне кажется (в Clojure я ничего не понимаю) вы «configuration programming» переизобрели, только зачем-то наследование оставили.
Что значит «инвертировать поведение»? Просто переопределить метод как в примере не достаточно?
Оказалось, что у вас летает 99% персонажей, для всех переопределять метод?
Поменять реализацию в корне (узле с пустым набором тегов) и переопределить для тех, кто летать не может.
Или добавить тег «overland», для которого сделать метод с запретом полетов и добавлять его к нелетающим персонажам.
и останется куча перегрузок у существующих персонажей, умеющих летать
Sign up to leave a comment.

Articles