Ко-вариантность и типы данных

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Тема вариантов в программировании вызывает кучу сложностей в понимании, по мне это проблема в том, что в качестве объяснения берут не всегда успешные метафоры - контейнеры.

Я надеюсь что может у меня получиться объяснить эту тему с другой стороны используя метафоры “присвоения” в разрезе лямбд.

Зачем вообще эта вариантность нужна ?

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

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

Flashback к типам

Типы данных сами по себе тоже не являются сверхважной темой, есть языки в которых тип данных не особенно нужны, например ассемблер, brainfuck, РЕФАЛ.

В том же РЕФАЛ или ассемблере очень легко перепутать к кому типу относиться переменная, и очень легко, например можно допустить что из одной строки я вычту другую строку, просто опечатка, никакого злого умысла.

В языках с поддержкой типов, компилятор увидел бы это опечатку и не дал бы мне скомпилировать программу, но… например JS

> 'str-a' - 'str-b'
NaN

JS (JavaScript) Спокойно этот код проглатывает, мне скажут что это не баг, это фича, ок, допустим, тогда я возьму Python

>>> 'str-a' - 'str-b'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for -: 'str' and 'str'

Или Java

jshell> "str-a" - "str-b"
|  Error:
|  bad operand types for binary operator '-'
|    first type:  java.lang.String
|    second type: java.lang.String
|  "str-a" - "str-b"
|  ^---------------^

То есть я клоню к тому, что считать багом или фичей - зависит от создателей языка.

А мне как пользователю например вообще без разницы на каком языке написана та или иная программа, мне важно чтоб она работала.

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

Еще пример: может быть такой мой сценарий, допустим вчера я написал на Groovy вот такой код

groovy> def fun1( a, b ){
groovy>   return a - b
groovy> }
groovy> println 'fun1( 5, 2 )='+fun1( 5, 2 )
groovy> println "fun1( 'aabc', 'b' )="+fun1( 'aabc', 'b' )
groovy> println 'fun1( [1,2,3,4], [2,3] )='+fun1( [1,2,3,4], [2,3] )

fun1( 5, 2 )=3
fun1( 'aabc', 'b' )=aac
fun1( [1,2,3,4], [2,3] )=[1, 4]

А сегодня так на JS в другом проекте

> fun1 = function( a, b ){ return a - b }
[Function: fun1]
> fun1( 5, 2 )
3
> fun1( 'aabc', 'b' )
NaN
> fun1( [1,2,3,4], [2,3] )
NaN

И вот таких не совпадений типов данных может быть много и мне действительно надо знать особенности того или иного языка.

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

Речь о типах данных

Вариантность как и ко/контр вариантность - это речь о типах данных и их отношениях между собой.

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

Один из способов избежать - это введение системы типов данных.

Вот пример на языке TypeScript

function sub( x : number, y : number ) {
    return x - y;
}

console.log( sub(5,3) )

Этот код успешно скомпилируется в JS.

А вот этот

function sub( x : number, y : number ) {
    return x - y;
}

console.log( sub("aa","bb") )

Уже не скомпилируется - и это хорошо:

> tsc ./index.ts
index.ts:5:18 - error TS2345: Argument of type 'string' is not assignable 
  to parameter of type 'number'.

5 console.log( sub("aa","bb") )
~~~~


Found 1 error.

В примере выше функция sub требует принимать в качестве аргументов переменные определенного типа, не любые, а именно number.

Контроль за типы данных я возлагаю уже компилятору TypeScript (tsc).

Инвариантность

Рассмотрим пока понятие Инвариантность, согласно определению

Инвариа́нт — это свойство некоторого класса (множества) математических объектов, остающееся неизменным при преобразованиях определённого типа.

Пусть A — множество и G — множество отображений из A в A. Отображение f из множества A в множество B называется инвариантом для G, если для любых a ∈ A и g ∈ G выполняется тождество f(a)=f(g(a)).

Очень невнятное для не посвященных определение, давай те чуть проще:

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

Рассмотрим пример операции присвоения переменной, в JS допускается вот такой код

> fun1 = function( a, b, c ){
... let r = b;
... if( a ) r = c;
... return r + r;
... }
[Function: fun1]
> fun1( 1==1, 2, 3 )
6
> fun1( 1==1, "aa", "b" )
'bb'
> fun1( 1==1, 3, "b" )
'bb'
> fun1( 1!=1, 3, "b" )
6
> fun1( 1!=1, {x:1}, "b" )
'[object Object][object Object]'

В примере переменная r - может быть и типа string и number и объектом, со стороны интерпретатора сказать какого типа данных возвращает функция fun1 нельзя, пока не запустишь программу.

Так же нельзя сказать какого типа будет переменная r. Тип результата и тип переменной r зависит от типов аргументов функции.

Переменная r по факту может иметь два разных типа:

  • В конструкции let r = b, переменная r будет иметь такой же тип, как и переменная b.

  • В конструкции r = c, переменная r будет иметь такой же тип, как и переменная c.

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

Можно наложить явным образом ограничения на вызов функции и проверять какого типа аргументы, например так:

> fun1 = function( a, b, c ){
... if( typeof(b)!=='number' )throw "argument b not number";
... if( typeof(c)!=='number' )throw "argument c not number";
... let r = b;
... if( a ) r = c;
... return r + r;
... }
[Function: fun1]
> fun1( true, 1, 2 )
4
> fun1( true, 'aa', 3 )
Thrown: 'argument b not number'

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

Другой же аспект, в том что операция +, - и др… при операциях над числами - возвращают числа - это и есть инвариантность (в широком смысле), а вот при над числами и строками или различными типами данных - результат уже менее предсказуем.

В языках со строгой типизацией операция конструкция let r = b и следующая за ней r = c не допустима, она может быть допустима если мы укажем типы аргументов.

Пример Typescript:

function fun1( a:boolean, b:number, c:number ){
    let r = b;
    if( a ) r = c;
    return r + r;
}

function fun2( a:boolean, b:number, c:string ){
    let r = b;
    if( a ) r = c;
    return r + r;
}

И результат компиляции

> tsc ./index.ts 
index.ts:9:13 - error TS2322: Type 'string' is not assignable to type 'number'.

9     if( a ) r = c;
~


Found 1 error.

Здесь в ошибки говориться явно, что переменная типа string не может быть присвоена переменной типа number.

Вариантность - в компиляторах, это проверка допустимости присвоения переменной одного типа значения другого типа.

Инвариантность - это такой случай, когда переменной одного типа присваивается (другая или эта же) переменная этого же типа.

Теперь вернемся к строгому определению: выполняется тождество f(a)=f(g(a))

То есть допустим у нас есть функции TypeScript:

function f(a:number) : number {
    return a+a;
}

function g(a:number) : number {
    return a;
}

console.log( f(1)===f(g(1)) )

Этот код - вот прям сторого соответствует определению.

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

function f(a:number) : number {
    return a+a;
}

function g(a:number) : number {
    return a-1;
}

let r = f(1)
r = f(g(1))

а такой код

function f(a:number) : number {
    return a+a;
}

function g(a:number) : string {
    return (a-1) + "";
}

let r = f(1)
r = f(g(1))

Уже невалиден (не корректен), так как:

  • функция g возвращает тип string

  • а функция f требует тип number в качестве аргумента

и вот такую ошибку обнаружит компилятор TypeScript.

Первый итог

Вариантность и другие ее формы, как например Ин/Ко/Контр вариантность - это качество операции присвоения значения переменной или передачи аргументов в функцию, в которой проверяется типы данных передаваемых/принимаемых в функцию и переменную.

Ко-вариантность

Для объяснения ко-вариантности и контр-вариантности, мне придется прибегнуть не к TypeScript, а к другому языку - Scala, причины я поясню ниже.

Вы наверно уже слышали про ООП и наследование, про различные принципы Solid

Ко-вариантность обычно объясняют через наследование, и что наследуются все свойства и методы родительского класса - это верно, рассмотрим пару примеров


Ко-вариантность это такое качество операции присвоения значения переменной значение переменной другого типа, при котором сохраняются все свойства и операции. —–

Есть несколько типов чисел и их можно расположить в следующей иерархии:

  1. Натуральные числа N

    • N натуральные числа, включая ноль: {0, 1, 2, 3, … }

    • N* натуральные числа без нуля: {1, 2, 3, … }

  2. Целые числа Z - обладают знаком (+/-) включают в себя натуральные

  3. Рациональные числа Q - дроби (два целых числа), включают в себя все бесконечное множество Z

  4. Вещественные числа R - это и рациональные и иррациональные числа (например ПИ, e, …)

  5. Комплексные числа C - числа вида a+bi, где a,b - вещественные числа, а i - мнимая единица

Давай те рассмотрим более подробно:

Числа мы можем условно расположить согласно такой иерархии

  • any - любой тип данных

    • number - некое число

      • int - целое число

      • double - (приближенное) дробное число

    • string - строка

так мы можем в языке TypeScript написать функции

function sum_of_int( a:int, b:int ) : int { return a+b; }
function sum_of_double( a:double, b:double ) : double { return a+b; }
function compare_equals( a:number, b:number ) : boolean { a==b }

в случае

let res1 : int = sum_of_int( 1, 2 )

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

Рассмотрим случай ко-вариантного присваивания

let res1 : number = sum_of_int( 1, 2 )
    res1          = sum_of_double( 1.2, 2.3 )

В данном примере res1 - это тип number.

В первом вызове res1 = sum_of_int( 1, 2 ), переменная res1 примет данные типа int, и это корректно, т.к. int это подтип number и по определению сохраняются все свойства и методы класса number

Во втором вызове res1 = sum_of_double( 1.2, 2.3 ) - переменная res1 примет данные типа double и это тоже корректно, так же по определению

О каких же операциях говорят что сохраняются? а все те же, мы все так же как и в первом, так и во втором случае можем выполнить операции проверки на равенство и д.р. для переменной res1:

let res1 : number = sum_of_int( 1, 2 )
let res2 : number = sum_of_doube( 1.2, 2.3 )
if( compare_equals(res1, res2) ){
  ...
}

ок, это работает, но компилятор нам нужен чтоб он за нас решал проблемы с типами, рассмотрим еще более “выпуклый” пример

Допустим у нас есть фигуры: прямоугольник Box и круг Circle

class Box {
    width : number
    height : number
    constructor( w: number, h: number ){
        this.width = w;
        this.height = h;
    }
}

class Circle {
    radius : number
    constructor( r: number ){
        this.radius = r
    }
}

И нам надо подсчитать сумму площадей, прямоугольники можно хранить в одном массиве, а круги в другом

let boxs : Box[] = [ new Box(1,1), new Box(2,2) ]
let circles : Circle[] = [ new Circle(1), new Circle(2) ]

Мы напишем 2 функции по подсчету площади, одну для прямоугольников, другую для кругов

function areaOfBox( shape:Box ):number { return shape.width * shape.height }
function areaOfCircle( shape:Circle ):number { return shape.radius * shape.radius * Math.PI }

Тогда для подсчета общей суммы площадей код будет примерно таким:

boxs.map( areaOfBox ).reduce( (a,b,idx,arr)=>a+b ) + 
circles.map( areaOfCircle ).reduce( (a,b,idx,arr)=>a+b )

Все выше выглядит ужасно, если вы знакомы с ООП или/и с базовой логикой (родовые, видовые понятия).

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

А по сему можно выделить общее абстрактное понятие Фигура и добавить в это абстрактное метод/свойство - area():number.

interface Shape {
    area():number
}

Вторым шагом, это указать что классы Box и Circle реализуют интерфейс Shape, и перенести areaOfBox, areaOfCircle как реализацию area.

class Box implements Shape {
    width : number
    height : number
    constructor( w: number, h: number ){
        this.width = w;
        this.height = h;
    }
    area():number {
        return this.width * this.height
    }
}

class Circle implements Shape {
    radius : number
    constructor( r: number ){
        this.radius = r
    }
    area():number {
        return this.radius * this.radius * Math.PI
    }
}

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

let shapes : Shape[] = [ new Box(1,1), new Box(2,2), new Circle(1), new Circle(2) ]
shapes.map( s => s.area() ).reduce( (a,b,idx,arr)=>a+b )

И в данном примере, ко-вариантность проявляется в инициализации массива

Массив определен как массив элементов типа Shape, мы инициализируем (т.е. присваиваем начальное значение) элементами другого типа под типа (Box, Circle).

Ключевой момент в том, что Box и Circle реализуют необходимые свойства и методы которые требует интерфейс Shape.

Компилятор отслеживает что присваиваемые значения реализуют заданное соглашение, т.е.

Компилятор по факту отслеживает конструкцию let a = b, и возможны несколько сценариев:

  1. переменная a и b - одного типа, тогда инвариантная операция присвоения

  2. переменная a является базовым типом, а переменная b - подтипом переменной a - тогда ко-вариантная операция присвоения

  3. переменная a является подтипом переменной b, а переменная b - базовым (родительским) типом - тогда это контр-вариантная операция - и обычно компилятор блокирует такое поведение.

  4. между переменными a и b - нет общих связей - и тут компилятор блокирует то же поведение.

И вот пример, по пробуем добавить еще один класс который не реализует интерфейс Shape

class Foo {
}

let shapes : Shape[] = [ new Box(1,1), new Box(2,2), new Circle(1), new Circle(2), new Foo() ]
shapes.map( s => s.area() ).reduce( (a,b,idx,arr)=>a+b )

Результат компиляции - следующая ошибка:

> tsc index.ts
index.ts:31:84 - error TS2741: Property 'area' is missing in type 'Foo' but required in type 'Shape'.

31 let shapes : Shape[] = [ new Box(1,1), new Box(2,2), new Circle(1), new Circle(2), new Foo() ]
                                                                                    ~~~~~~~~~

index.ts:2:5
    2     area():number
        ~~~~~~~~~~~~~
    'area' is declared here.


Found 1 error.

Для типа Foo не найдено свойство area, которое определенно в типе Shape.

Тут уместно упомянуть о SOLID

L - LSP - Принцип подстановки Лисков (Liskov substitution principle): «объекты в программе должны быть заменяемыми на экземпляры их подтипов без изменения правильности выполнения программы». См. также контрактное программирование.

Контр-вариантность

Контр-вариантность, уже сложнее объяснить, для меня примеры с длегатами действовали на нервы, я же разобрался на примере с лямбд.

В качестве примера, возьму язык Scala и подробно попытаюсь его разобрать:

package xyz.cofe.sample.inv

object App {
  // Функция, на вход String, на выход Boolean, или кратко: (String)=>Boolean 
  def strCmp(a:String):Boolean = a.contains("1")

  // Функция, на вход Int, на выход Boolean, или кратко: (Int)=>Boolean
  def intCmp(a:Int):Boolean = a==1

  // Функция, на вход String, на выход Boolean, или кратко: (Any)=>Boolean
  def anyCmp(a:Any):Boolean = true

  def main(args:Array[String]):Unit = {
    
    // Инвариантное присвоение Boolean = Boolean
    val call1 : Boolean = strCmp("a")
    
    // Ко-вариантное присвоение Any = Boolean
    val call2 : Any = strCmp("b")

    // Инвариантное присвоение: (String)=>Boolean = (String)=>Boolean
    val cmp1 : (String)=>Boolean = App.strCmp;

    // Ко-вариантное присвоение (String)=>Boolean = (Any)=>Boolean
    val cmp2 : (String)=>Boolean = App.anyCmp

    // Инвариантное присвоение: (String)=>Boolean = (String)=>Boolean
    val cmp3 : (Any)=>Boolean = App.anyCmp

    // !!!!!!!!!!!!!!!!!!!!!!!
    // Тут будет ошибка
    // Контр-вариантное присвоение (Any)=>Boolean = (String)=>Boolean
    val cmp4 : (Any)=>Boolean = App.strCmp
  }
}

Что нужно знать о Scala:

  • Тип Any - это базовый тип для всех типов данных

  • Тип Int, Boolean, String - это подтипы Any

  • Лямбды то же являются типами, в смысле типы их аргументов и результатов проверяет компилятор

  • Тип лямбды записывается в следующей форме: (тип_аргументов,через_запятую)=>тип_результата

  • Любой метод с легкостью преобразуется в лямбду переменная = класс.метод / переменная = объект.метод

  • val в Scala, то же что и const в JS

В примере мы можем увидеть уже знакомые

Инвариантность в присвоении переменных:

// Инвариантное присвоение Boolean = Boolean
val call1 : Boolean = strCmp("a")

// Инвариантное присвоение: (String)=>Boolean = (String)=>Boolean
val cmp1 : (String)=>Boolean = App.strCmp;

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

Ожидаемый тип  (String)=>Boolean
Присваемый тип (String)=>Boolean

Ко-вариантность

// Ко-вариантное присвоение Any = Boolean
val call2 : Any = strCmp("b")

// Ко-вариантное присвоение (String)=>Boolean = (Any)=>Boolean
val cmp2 : (String)=>Boolean = App.anyCmp

Если в случае присвоения call2, тут все понятно, то может быть непонятно с cmp2.

Ожидаемый тип  (String) => Boolean
Присваемый тип (Any)    => Boolean

Внезапно отношение String -> к -> Any становится другим - контр-вариантным.

В этом месте, уместно задаться WTF? - Все нормально!

Рассмотрим функции выше

// Функция, на вход String, на выход Boolean, или кратко: (String)=>Boolean 
def strCmp(a:String):Boolean = a.contains("1")

// Функция, на вход String, на выход Boolean, или кратко: (Any)=>Boolean
def anyCmp(a:Any):Boolean = true

При вызове cmp2( "abc" ) аргумент "abc" будет передан в anyCmp(a:Any), а по скольку String является под типом Any, то аргумент не дано преобразовывать и можно передать как есть.

Иначе говоря вызов anyCmp( "string" ) и anyCmp( 1 )anyCmp( true ) - со стороны проверки типов допустимы операции, по скольку

  • принимаемые аргументы являются подтипами для принимающей стороны, тела функции

  • тип принимаемого аргумента является родительским типом (надтипом) со стороны вызова функции

Т.е. можно при передаче аргументов, действуют ко-вариантность со стороны принимающей, а со стороны передающей контр-вариантность.

Еще более наглядно это можно выразить стрелками:

Операция присвоения должна быть ко-вариантна или инвариантна

assign a <- b

А операция вызова функции на оборот - контр-варианта или инвариантна

call a -> b

Этим правилом руководствуются многие компиляторы, и они определяют функции так:

  • Операции передачи аргументов в функции по умолчанию являются контр-вариантны, со стороны вызова функции

  • Операции присвоения результат вызова функции по умолчанию является ко-вариантны, со стороны вызова функции

Я для себя запомню так

Почему Scala, а не TypeScript

К моему удивлению TypeScript версии 4.2.4 не отрабатывает контр-вариантность в случае функций/лямбд

Вот мой исходник

interface Shape {
    area():number
}

class Box implements Shape {
    width : number
    height : number
    constructor( w: number, h: number ){
        this.width = w;
        this.height = h;
    }
    area():number {
        return this.width * this.height
    }
}

class Circle implements Shape {
    radius : number
    constructor( r: number ){
        this.radius = r
    }
    area():number {
        return this.radius * this.radius * Math.PI
    }
}

class Foo {
}

const f1 : (number)=> boolean = a => true;
const f2 : (object)=> boolean = a => typeof(a)=='function';
const f3 : (any)=>boolean = f1;
const f4 : (number)=>boolean = f3;

const _f1 : (Box)=>boolean = a => true
const _f2 : (any)=>boolean = _f1
const _f3 : (Shape)=>boolean = _f1

В строке const f3 : (any)=>boolean = f1; и в const _f3 : (Shape)=>boolean = _f1 (а так же предыдущей) компилятор по моей логике должен был ругаться, но он этого не делал

user@user-Modern-14-A10RB:03:14:17:~/code/blog/itdocs/code-skill/types:
> ./node_modules/.bin/tsc -version
Version 4.2.4
user@user-Modern-14-A10RB:03:16:53:~/code/blog/itdocs/code-skill/types:
> ./node_modules/.bin/tsc --strictFunctionTypes index.ts 
user@user-Modern-14-A10RB:03:18:26:~/code/blog/itdocs/code-skill/types:
> ./node_modules/.bin/tsc --alwaysStrict index.ts 
user@user-Modern-14-A10RB:03:19:04:~/code/blog/itdocs/code-skill/types:

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

Ко-вариантность/Контр-вариантность и типы

Еще одна важная оговорка связанная с типами и ООП.


Вариантность это не только про иерархию наследования!


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

Ко-Вариантность - это такое качество операции присвоения, когда целевой тип переменной совместим с исходным типом значения.

Контр-вариантность - ровно та же ситуация с противоположным знаком.

Тут надо дать пояснение слова совместимость

Пример с кругами и прямоугольниками может быть написан на языке C или ассемблера, или JS ранних версий, в которых нет понятия классов, но при этом оно все так же будет работать.

ООП с наследованием - это всего лишь способ, задать иерархию реальных типов объектов.

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

Например я могу выстроить разные наборы иерархий для одних и тех же сущностей:

Например:

  • Человек (общий класс)

    • Национальность (под класс)

      • Социальный статус (под класс)

или наоборот

  • Человек (общий класс)

    • Пол (под класс)

      • Социальный статус (под класс)

Это я клоню к тому, что для одной и той же сущности может существовать множество способов квалификации.

И один из подходов - эту сложную сушность (как например человек) можно рассматривать с различных сторон - и вот уже эти стороны можно выделить в виде интерфейсов.

А уже в рамках того или иного интерфейса описывать интересующие свойства и методы для решения практических задач.

Вариантность - это в первую очередь наличие интересующих нах свойств/методов для наших задач. И это механизм контроля со стороны компилятора, для гарантии наличия этих свойств.

Так, например тот или иной объект может быть не только каким либо под классом, но и реализовывать (через интерфейсы) интересующие нас свойства/методы - именно это я понимаю под словом совместимость.

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

Источник: https://habr.com/ru/post/560936/


Интересные статьи

Интересные статьи

Когда TomTom проводил анализ мирового дорожного трафика за 2020 год, обнаружилось, что результаты анализа отражают ход пандемии, изменение привычного образа жизни и соблю...
Каждый раз при необходимости взаимодействия с базами данных появляются три вопроса: - Какой подход использовать при разработке: java-first или database-first? Писать снач...
В стандарте ECMAScript 2015, известном как ES6, появилось много новых JavaScript-коллекций, таких, как Map, Set, WeakMap и WeakSet. Они, судя по всему, стали отличным дополнением к ...
Здравствуйте. Я уже давно не пишу на php, но то и дело натыкаюсь на интернет-магазины на системе управления сайтами Битрикс. И я вспоминаю о своих исследованиях. Битрикс не любят примерно так,...
Реализация ORM в ядре D7 — очередная интересная, перспективная, но как обычно плохо документированная разработка от 1с-Битрикс :) Призвана она абстрагировать разработчика от механики работы с табл...