stdray: (Default)
[personal profile] stdray
В вики Nemerle существует вот такая страничка, на которой рассказывается, как могли бы вести себя трейты, будь они реализованы в языке. Но пока что "not yet done", хотя тема неоднократно поднималась на форуме. Как я полагаю, это все следствие размышлений на тему "Как бы нам вернуть обратно множественное наследование". То есть сейчас в дотнетах оно есть, но в значительно урезанном виде: интерфейсы позволяют наследовать только требования к объектам, а уж реализацию приходится пилить вручную, что конечно же раздражает.

Мне в этом описание понравилась идея выставлять требования для объектов, к которым возможно "подселять" трейты. Однако, мне не очень нравится идея реализовывать это дело внутри интерфейсов, поскольку такой объект будет некорректным с точки зрения, допустим, C#. К тому же такие трейты не смогут иметь собственное состояние, что очень сильно ограничивает варианты их использования.

Если не фокусироваться на описанном синтаксисе, то можно сказать, что задача реализации примесей состоит из трех этапов:
- проверка требований
- обеспечение отношения подтипирования
- наследование реализации
При этом, можно считать, что наследование реализации - это такое хитрожопый макрос, занимающийся переписыванием полей из одного определения в другое. А поскольку Nemerle обладает развитой макросистемой, вполне возможно такое реализовать.

Для описания примеси я использую модуль, помеченный атрибутом [DefMixin], внутри которого описывается интерфейс (он представляет собой требования) и класс (он представляет реализацию). Простейшее описание примеси может выглядеть так:

[DefMixin]

public module Foo {

public interface Require {

getX() : int;

setX(v: int) : void;

}

public class Provide[T] where T: Require {

public inc() : void { self.setX(self.getX() + 1); }

}

}


На этом примере видно, что из миксины можно вызывать методы объявленные в интерфейсе с требованиями. Для этого используется идентификатор self, который позже будет связан с объектом, для которого выполняется подмешивание. Подселить же примесь к объекту можно с помощью макроса [AddMixin], передав ему в качестве аргумента название модуля, в котором определена примесь.

[AddMixin(Foo)]

public class Bar {

mutable x = 0;

public getX() : int { x }

public setX(v: int) : void { x = v }

}


В результате можно вызывать на объекте, как его собственные методы, так и подмешанные

def bar = Bar();

bar.inc();

bar.inc();

WriteLine <| bar.getX(); // out: 2


При этом на этапе компиляции происходит проверка, соответствует ли композитный объект требованиям примеси:

Кроме того, поскольку CLR поддерживает параметрический полиморфизм, примеси умеют с ним работать. Например так:

[DefMixin]

public module Foo {

public interface Require[U] where U : new() {

ValU : U { get ; set; }

}

public class Provide[T, U]

where T: Require[U] //все это можно скопировать

where U : new() { //из описания ограничений

public pairWithDefault() : U * U { (self.ValU, U()) }

}

}

[AddMixin(Foo)]

public class Bar[U] where U : new() {

public ValU : U { get; set }

}

module Program {

Main() : void {

def bar = Bar();

bar.ValU = 999;

WriteLine <| bar.pairWithDefault(); // out: (999, 0)

}

}


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

[AddMixin(Foo[int])]

public class Bar {

public ValU : int { get; set; }

}


Я думаю, что даже с учетом некоторого оверхеда на описание таких примесей (вопрос присахаривания этого дела открыт, и возможно даже получится привести к виду из описания трейтов), предоставляемые ими возможности являются весьма заманчивыми. Можно один раз описать некоторое поведение, а потом просто наделять им свои объекты. Насколько я могу судить, в комьюнити Nemerle для описания какого-то специфичного поведения пишут макросы. Но, полагаю, если можно обойтись такой хитрой реализацией множественного наследования, лучше именно так и поступить. Это достаточно общий механизм, допускающий нормальную отладку подмешанного кода через стандартный отладчик студии, банальной установкой брек-пойнта в интересующем методе миксины.

Описанные выше примеры выглядят весьма искуственно. Но начиная писать реализацию примесей я думал о более-менее конкретных вещах. Меня беспокоит состояние ORM в дотнете, как и отношение к ним. То есть существуют разного рода Entity, NHibernate и так далее. И вроде общая тендеция такова, что именно их надо использовать для описания модели данных приложения. А у меня почему-то подобное не прокатывает. Если есть желание использовать все возможности языка для объектной декомпозиции задачи, то придется постоянно биться головой об ограничения накладываемые конкретным фреймфорком. Который кроме всего прочего, привносит в модель сложное и неявное поведение. Может приводить ко всяким казусам, вроде проблемы N+1 запросов при ленивой загрузке и так далее.

Мне, на данный момент, самым адекватным представляется решение BLToolkit. Это очень простая и удобная ORM, которая ведет себя предсказуемым образом и предоставляет возможности гибкой работы с базой данных. Вот здесь можно увидеть примеры написания insert'ов и update'ов. Мне нравится, что апдейты можно свободно выполнять на стороне сервера, что можно самому управлять процессом сохранения информации. А не как это бывает с более толстыми фреймворками - что-то загрузил, что-то поменял, как теперь сохранить изменения единственного конкретного объекта - непонятно, поскольку метод SubmitChanges/SaveChanges один на всех. Таким образом, можно сказать, что BLToolkit является просто удобным средством написания типизируемых автоматически проверяемых SQL запросов. Где вместо никак не структурируемого SQL, можно использовать няшный IQuerable и удобно манипулировать запросами.

Однако, BLTookit не умеет отслеживать изменения, не подерживает ленивую загрузку, его объекты не имеют метки состояния (новый / изменен / удален). А поскольку ручная реализация подобного поведения является весьма затратной, часто приходится выбирать что-то более тяжеловесное, подписываясь решать проблемы, которых вообщем-то на начальном этапе не видно.

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

[DefMixin]

module ChangeTracking {

public interface Require : INotifyPropertyChanged { }

public class Provide[T] where T: Require {

_changes : HashSet[string] = HashSet();

propertyChanged(_ : object, e : PropertyChangedEventArgs) : void {

_ = _changes.Add(e.PropertyName);

}

this() {

self.PropertyChanged += propertyChanged;

}

public ChangedProperties : list[string] {

get { _changes.NToList() }

}

}

}


Так можно определить логику, которая позволяет отмечать объект как удаленный:

[DefMixin]

module Deletable {

public interface Require { }

public class Provide[T] where T : Require {

private mutable isDeleted = false;

public IsDeleted : bool { get { isDeleted } }

public Delete() : void { isDeleted = true; }

}

}


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

variant EntityState { | UnModified | Modified | Deleted }

[DefMixin]

module EntityWithState {

public interface Require {

ChangedProperties : list[string] { get; }

IsDeleted : bool { get; }

}

public class Provide[T] where T : Require {

public State : EntityState {

get {

match(self.IsDeleted, self.ChangedProperties) {

| (true, _) => EntityState.Deleted();

| (_, []) => EntityState.UnModified();

| (_, _) => EntityState.Modified();

}

}

}

}

}


Это выглядит несколько громоздко, но получать композитные объекты, реализующие все вышеописанное поведение, становится достаточно просто:

[ImplementsNotifyPropertyChanged,

AddMixin(ChangeTracking),

AddMixin(Deletable),

AddMixin(EntityWithState)]

class Point3d {

public x : int { get; set; }

public y : int { get; set; }

public z : int { get; set; }

}


И тогда можно оценить результаты работы


Мне кажется, что выглядит здорово, хотя на текущий момент существует еще ряд проблем.

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

[DefMixin]

module EntityWithState {

public interface Require { }

public class Provide[T] where T : Deletable.Mixin[T], ChangeTracking.Mixin[T] {

public State : EntityState {

get {

match(self.IsDeleted, self.ChangedProperties) {

| (true, _) => EntityState.Deleted();

| (_, []) => EntityState.UnModified();

| (_, _) => EntityState.Modified();

}

}

}

}

}


Здесь Deletable.Mixin[T] и ChangeTracking.Mixin[T] - это просто интерфейсы, являющиеся суммой требований миксины и предоставляемых ею возможностей. Однако компилятор ругается

D:\Projects\nemerle.mixins\ConsoleApplication1\Main.n(115,5): error : recursive type or recursive constraint detected (ChangeTracking.Mixin.[T])

D:\Projects\nemerle.mixins\ConsoleApplication1\Main.n(115,5): error : recursive type or recursive constraint detected (Deletable.Mixin.[T])


При том объекты с подселенными миксинами этот интерфейс реализуют. И можно заставить такие ограничения работать, объявив один дополнительный тип-параметр, но тогда число параметров будет постоянно расти, что мне не нравится. На данный момент, я думаю, что для миксин, которые ни в типах параметров методов, ни в типе возвращаемого значения не используют данный полиморфный тип, его можно опускать при генерации интерфейса Mixin.
Во-вторых, я размышлял над тем, имеет ли смысл подселять статические методы. Мне показалось, что иногда это может быть удобным, но может и приводить к неразрешимым конфликтам.
В-третьих, у меня пока не сделано разрешение конфликтов наследуемых членов. Конфликты могут возникать как с членами самого композитного класса, так и между методами миксин. Решение в данном случае простое, поскольку вместе с примесью идет ее собственный интерфейс Mixin, при конфликте можно просто делать явную реализацию проблемного мембера. Таким образом конфликты будут разрулены и любое поведение будет доступно через безопасное уточнение типа. По сути у меня все есть, только надо научиться определять эквивалентность сигнатур.
В-четвертых, в Nemerle, как я выяснил, по аналогии с C# существуют все эти богомерзские out/ref параметры, а мой макрос AddMixin пока не учитывает их при копировании.
В-пятых, пока не до конца понятно, что делать с атрибутами и макро-атрибутами, которыми можно обвешать модуль с миксиной, интрефейс с требованиями, класс с реализацией, а так же все доступные меберы данных типов. Наверное тоже надо копировать, но уверенности нет.


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

December 2019

S M T W T F S
1234567
891011121314
15161718192021
222324252627 28
293031    

Most Popular Tags

Style Credit

Expand Cut Tags

No cut tags