LINUX.ORG.RU

Выбор более специфичного инстанса по констрейнам

 , ,


0

2

Господа гусары, занимаюсь приседаниями с системой типов Haskell. Решил соорудить эдакую систему кастования типов. Что имею в виду: есть тип, есть его субтип, построенный на базе родителя, субтип ведет себя так-же, как родитель и может быть безопасно преобразован в родителя, но обратно не каждый родитель кастуется к указанному субтипу.

Вот код

{-# LANGUAGE MultiParamTypeClasses, FlexibleInstances, FunctionalDependencies, FlexibleContexts, UndecidableInstances, OverlappingInstances, ScopedTypeVariables #-}

class SubtypeOf a b | a -> b where
  upcast :: a -> b
  downcastSafe :: b -> Maybe a
  downcast :: b -> a
  downcast b = case downcastSafe b of
    Nothing -> error $ "can not downcast the value"
    Just a -> a

  
data Range i a = Range {unRange :: a}
                 deriving Show

class Rangeable i where
  lowLim :: (Ord a, Num a) => t i a -> a
  highLim :: (Ord a, Num a) => t i a -> a

instance (Rangeable i, Num a, Ord a) =>  SubtypeOf (Range i a) a where
  upcast = unRange
  downcastSafe b | (b >= lowLim p) && (b <= highLim p) = Just p
                 | otherwise = Nothing
    where
      p :: Range i a
      p = Range b


data R1 = R1

instance Rangeable R1 where
  lowLim _ = 0
  highLim _ = 10

data R2 = R2

instance Rangeable R2 where
  lowLim _ = -10
  highLim _ = 100

data Multiple i a = Multiple {unMultiple :: a}
                    deriving Show

class Multipable i where
  mValue :: (Num a) => t i a -> a

data M2 = M2

instance Multipable M2 where
  mValue _ = 2

instance (Multipable i, Integral a) => SubtypeOf (Multiple i a) a where
  upcast = unMultiple
  downcastSafe b | b `mod` (mValue p) == 0 = Just p
                 | otherwise = Nothing
    where
      p :: (Multiple i a)
      p = Multiple b

instance (Multipable i, RealFrac a) => SubtypeOf (Multiple i a) a where
  upcast = unMultiple
  downcastSafe b | x == 0 = Just p
                 | otherwise = Nothing
    where
      x = d - (fromIntegral $ floor d)
      d = b / (mValue p)
      p :: Multiple i a
      p = Multiple b

SubtypeOf связывает два типа, a - субтип, b - родитель (возможно я не совсем правильно употребляю здесь слово субтип). Range - тип, хранящий число в определенном интервале, тип имеет фантомный тип. R1 и R2 - фантомные типы для указания диапазона в котором находится число. Инстанс SubtypeOf для (Range i a) a написан в обобщенном виде, так, что можно кастовать к Range с любым фантомным типом инстанцирующим Rangeable, и все работает.

Теперь, захотел я сделать тип «число кратное N» с фантомынм типом, указывающим это самое N. Тоже самое - Multiple - наш тип «число кратное N», M2 - фантомный тип, говорящий, что число должно быть кратно 2, дальше проблемы.

Кратность числа для целых и дробных чисел проверяется по-разному: для целых есть операция вычисления остатка от деления, если она == 0, то число кратное делителю. Для дробных нужно поделить, а потом проверить, равна ли дробная часть результата нулю. И нам нужно два инстанса для SubtypeOf с целым типом-родителем и с дробным.

instance (Multipable i, Integral a) => SubtypeOf (Multiple i a) a where

instance (Multipable i, RealFrac a) => SubtypeOf (Multiple i a) a where

Однако, компилятор считает, что эти инстансы дублирующиеся

/home/razor/casting.hs:51:10:
    Duplicate instance declarations:
      instance [overlap ok] (Multipable i, Integral a) =>
                            SubtypeOf (Multiple i a) a
        -- Defined at /home/razor/casting.hs:51:10
      instance [overlap ok] (Multipable i, RealFrac a) =>
                            SubtypeOf (Multiple i a) a
        -- Defined at /home/razor/casting.hs:59:10
Failed, modules loaded: none.

Как так ? Float инстанцирует RealFrac но не инстанцирует Integral, Integer - все наоборот. По логике, мы могли бы использовать такие инстансы, и ошибка должна была бы возникать только если a инстанцирует сразу и то и другое, так, что компилятор не может угадать, какой инстанс использовать. Но таких типов даже нету, все типы либо Integral, либо RealFrac. Поясните, почему так нельзя, и как можно ?

Кастую KblCb quasimoto adzeitor Miguel dmitry_malikov qnikst

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

Представь себе ситуацию. Ты запихнул весь этот код в какой-то модуль. Откомпилировал, причём даже не экспортируя из него класс SubtypeOf. Модуль оформил в библиотеку и выложил на Hackage.

Затем кто-то взял твою библиотеку, импортировал модуль, и попытался использовать со своим собственным типом, который у него таки является инстансом и Integral, и RealFrac. И что, компилятор должен в этот момент выдать сообщение о перекрывающихся инстансах некоего класса, о котором этот человек вообще ничего не знает?

Miguel ★★★★★
()
Ответ на: комментарий от Miguel

Если я не экспортировал SubtypeOf, то этот человек не сможет использовать upcast/downcast для своего типа, а значит, компилятору не нужно делать инстанс для этого типа. Никакого противоречия.

Я так понял, это вопрос не семантики, а реализации компиляции в GHC ?

s9gf4ult ★★
() автор топика
Ответ на: комментарий от s9gf4ult

Если я не экспортировал SubtypeOf, то этот человек не сможет использовать upcast/downcast для своего типа, а значит, компилятору не нужно делать инстанс для этого типа. Никакого противоречия.

Да можешь хоть расписку нотариально заверенную оформить, компилю это побоку. Он видит что переданный тип потенциально может иметь оба инстанса, а значит и породить неопределенность.

Я так понял, это вопрос не семантики, а реализации компиляции в GHC ?

Это вопрос архитектуры и кода.

anonymous
()
Ответ на: комментарий от anonymous

компилю это побоку, тип потенциально может иметь оба инстанса

Ну что за бред? Потенциально он может иметь что угодно, это ленивый язык или где ? Я ожидал, что инстансы выводятся для конкретных типов на основании констрейнов инстансов и классов. Если не однозначный вывод - тогда да, ошибка компиляции, но если мы даже не пользуемся методами тайпкласса, то и инстансов для конкретного типа выводить-то и не нужно. Где я не прав ?

Это вопрос архитектуры и кода.

Начась икспертиза ... Это вообще задачка обстрактная, не привязанная к чему-либо. Я же сказал, что приседаю с типами.

Вообще я придумал решение: сделать два типа MultipleInt и MultipleFrac, и инстанцировать для каждого из них SubtypeOf. Только это бойлерплейтно, и покуда у нас есть два типа, то тут напрашивается использование Type Families. Как обобщенно такое сделать ? Я имею в виду обобщенное кастование типов в целом.

s9gf4ult ★★
() автор топика
Ответ на: комментарий от s9gf4ult

Я ожидал, что инстансы выводятся для конкретных типов на основании констрейнов инстансов и классов.

Инстансы выбираются на основании типов, а не их инстансов.

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

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

fmap
()
Ответ на: комментарий от s9gf4ult

Потенциально он может иметь что угодно, это ленивый язык или где ?

Язык ленивый на уровне ЗНАЧЕНИЙ.

Miguel ★★★★★
()

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

Miguel ★★★★★
()

тип
субтип

Ты не в теме, настоящий штангист должен говорить «тайп», «сабтайп», а также «тайпчек», «тайпкласс», «тайптег» и так далее. «Тип» говорят только недорезанные Луговским жабабыдлокодеры.

А по теме — не понимаю, в чём проблема?Делаешь пандорический захват, лифтишь в монаду, потом строишь рекурсивную схему (здесь подойдёт зигохистоморфный препроморфизм) как монадический трансформер из категории эндофункторов, и метациклически вычисляешь результат. Любой второкурсник справится. А если делать на анафорических лямбдах — так задачка вообще на пять минут.

anonymous
()
Ответ на: комментарий от s9gf4ult

Вообще я придумал решение: сделать два типа MultipleInt и MultipleFrac, и инстанцировать для каждого из них SubtypeOf.

Ещё можно написать четыре инстанса SubtypeOf - Int, Integer, Float, Double, два раза продублировав код.

Или делать как Oleg с SPJ, то есть использовать фантомные классы типов уже:

{-# LANGUAGE
    MultiParamTypeClasses, FunctionalDependencies, FlexibleInstances,
    UndecidableInstances, ScopedTypeVariables, TypeOperators
  #-}

import Data.Maybe

__ :: t
__ = error "phantom type used as value"

class a :< b where
  sup :: a -> b
  sub :: b -> Maybe a

тут более явный __ вместо undefined и минимальный класс с TypeOperators, производные от минимального класса дописываются как

downcast :: (a :< b) => b -> a
downcast = fromMaybe (error "can not downcast the value") . sub

(fromMaybe!). Теперь вариант :< которому передаётся _представление_ класса (Integral, ...) в виде типа, так что он сможет их различать в RHS

class (a :<: b) cls | a -> b where
  sup_ :: cls -> a -> b
  sub_ :: cls -> b -> Maybe a

Отображение из типов в это представление (по хорошему - представление должно быть значением типа «Представление», но тогда это отображение из типов в значения в type-level - форма зависимых типов которую GHC ещё не покрыл)

class b :@ cls | b -> cls

то есть оно утверждает, что тип b является инстансом класса который представляется типом cls. После чего для всех таких инстансов:

instance (b :@ cls, (a :<: b) cls) => a :< b where
  sup = sup_ (__ :: cls)
  sub = sub_ (__ :: cls)

Потом твой код (newtype, M2 - пустой):

newtype Multiple i a = Multiple { fromMultiple :: a }
  deriving Show

class Multipable i where
  mValue :: Num a => t i a -> a

data M2

instance Multipable M2 where
  mValue _ = 2

утверждения про инстансы:

data IntegralC
data RealFracC

instance Int :@ IntegralC
instance Integer :@ IntegralC
instance Float :@ RealFracC
instance Double :@ RealFracC

и инстансы которые ты хочешь написать:

instance (Multipable i, Integral a) => (Multiple i a :<: a) IntegralC where
  sup_ _ = fromMultiple
  sub_ _ b | b `mod` (mValue p) == 0 = Just p
           | otherwise = Nothing
    where p = Multiple b :: Multiple i a

instance (Multipable i, RealFrac a) => (Multiple i a :<: a) RealFracC where
  sup_ _ = fromMultiple
  sub_ _ b | d - (fromIntegral $ floor d) == 0 = Just p
           | otherwise = Nothing
    where d = b / (mValue p)
          p = Multiple b :: Multiple i a

Теперь можно использовать (:<), sup, sub, downcast и не видеть (:<:) и прочих. Если появятся ещё инстансы у Integral/RealFrac - нужно написать ещё строчек instance ... :@ IntegralC, ... По крайней мере, так не нужно дублировать сами инстансы (:<:) и типы вроде Multiple.

Как обобщенно такое сделать ? Я имею в виду обобщенное кастование типов в целом.

В runtime? Это Data.Typeable. А в type-level - выкручиваться как-то и бороться с ограничениями GHC. Например - http://okmij.org/ftp/Haskell/typecast.html:

class TypeCast   a b   | a -> b, b->a   where typeCast   :: a -> b
class TypeCast'  t a b | t a -> b, t b -> a where typeCast'  :: t->a->b
class TypeCast'' t a b | t a -> b, t b -> a where typeCast'' :: t->a->b
instance TypeCast'  () a b => TypeCast a b where typeCast x = typeCast' () x
instance TypeCast'' t a b => TypeCast' t a b where typeCast' = typeCast''
instance TypeCast'' () a a where typeCast'' _ x  = x

(что бы это ни значило) и далее по ссылкам про OOHaskell и HList.

quasimoto ★★★★
()
Ответ на: комментарий от quasimoto

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

фантомный тайпкласс - это класс без методов ?

class (a :<: b) cls | a -> b where

Это тайпкласс над тремя типами так ? a b и cls, cls тут просто тип. При этом, инстансы именно этого класса будут «делать работу».

class b :@ cls | b -> cls
Это наш фантомный тайпкласс :@, связывающий тип - родитель и фантомный тип, который нам нужен для разделения реализации даункаста ...

instance (b :@ cls, (a :<: b) cls) => a :< b where

И это единственный его инстанс так ?

instance Int :@ IntegralC
instance Integer :@ IntegralC
instance Float :@ RealFracC
instance Double :@ RealFracC
Явно привязываем типы - родители к реализации даункаста.

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

В runtime? Это Data.Typeable

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

выкручиваться как-то и бороться с ограничениями GHC

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

s9gf4ult ★★
() автор топика
Ответ на: комментарий от s9gf4ult

Евгений Ваганыч, залогиньтесь.

А разве можно вести себя по-другому на сайте, где Лисп и Хаскель воспринимают всерьёз?

anonymous
()
Ответ на: комментарий от anonymous

Можно, можно покинуть этот сайт и вернуться в свой уютненький m$dn, там тебя поддержат.

s9gf4ult ★★
() автор топика

А вот чтоб таких вот реакций, как вот эта, не было, ОП, тебе надо не просто сухо изложить факты: чем занимаешься, как этим занимаешься, катаморфизмы-комонады-поехали — а ЗАИНТЕРЕСОВАТЬ! Чтобы местные статикодети посмотрели и сказали: «Йоба, да этот чувак реально крут!»; чтобы у динамикодебилов полопались пердаки от осознания того, что так они никогда не смогут; чтоб было ух, чтоб жахнул — и все охуели.

А у тебя что? «Господа гусары, занимаюсь приседаниями с системой типов Haskell». Похлёбываю борщ. Подрачиваю на аниме. Решил соорудить систему кастования типов комонады субтипы безопасно анаморфизм, наконец. Ну что это? Кто это будет читать? Ты один и будешь.

anonymous
()

Вдогонку. Хаскель — не для типодрочества, а для написания простых и понятных программ. Если хочешь писать кластеры метапарадигм на типах, возьми Агду, там это по-человечески сделано, а на Хаскеле всё, что сложнее моноида в категории эндофункторов — уныло, скучно, непонятно, вырвиглазисто и никто в этой хуйне копаться не будет.

anonymous
()
Ответ на: комментарий от anonymous

Хаскель — не для типодрочества, а для написания простых и понятных программ.

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

anonymous
()

Однако, компилятор считает, что эти инстансы дублирующиеся

Компилятор же не смотрит на констрейнты, когда выбирает инстанс, а SubtypeOf (Multiple i a) и SubtypeOf (Multiple i a) явно одно и то же. Я с этим столкнулся и дропнул типодрочество на хаскеле, потому что хуита.

anonymous
()
Ответ на: комментарий от anonymous

дропнул типодрочество на хаскеле

И выбрал Агду ? Хотя и на хачкеле тоже не плохо, вон, тип выше показал как надо. Может написать в рассылку, типа, почему до сих пор нет полиморфных инстансов в моем хаскеле ...

s9gf4ult ★★
() автор топика
Ответ на: комментарий от s9gf4ult

фантомный тайпкласс - это класс без методов ?

В данном случае нормальный класс который нужен это (:<), но с ним не получается, так как GHC различает инстансы по RHS, а с ним они будут одинаковы. Поэтому заводится (:<:) с дополнительным параметром по которому будет проводиться различие в RHS, дальше пишутся рабочие инстансы (:<:) в которых этот cls используется фантомно (в compile time), но не используется непосредственно (в runtime, там прочерк _ на этом параметре), то есть, по идее, так же как фантомный тип может иметь конструкторы, фантомный класс может иметь методы, главное, чтобы у типа фантомный параметр в LHS не участвовал в RHS'es, а у класса он был параметром класса, но не использовался в рантаме кроме как type level вещь (со ScopedTypeVariables). Тогда можно сказать что у нас «фантомный класс».

Вообще, если пытаться осмыслить понятие класса типов - это просто запись:

record _≤_ (A B : Set) : Set where
  field
    -- интерфейс
    sup : A → B
    sub : B → Maybe A
    -- тут можно дописать теорем (те же функции, но зависимые от sup/sub) которым он должен удовлетворять
    -- инстанс должен реализовать sup/sub и доказать что все теоремы для них работают

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

data SubtypeRelation a b = SubtypeRelation {
  _sup :: a -> b,
  _sub :: b -> Maybe a
  -- зависимой записи сделать не получится, так что без теорем, просто интерфейс в виде упакованных в структуру сигнатур
  }

В данном случае это даже «тип», а не «класс».

Если теперь сделать

data Functor f = Functor {
  _map :: forall a b. (a -> b) -> f a -> f a
  }

то хаскелю пофиг - тоже тип, в агде это уже класс, то есть не помещается в универсум типов Set (если поместить - будут противоречия), помещается в универсум классов (сам Set - тоже класс) Set₁:

record Functor (F : Set → Set) : Set₁ where
  field
    -- интерфес
    map : ∀ {A B} → (A → B) → F A → F B
    -- функторные законы, например -- http://www.linux.org.ru/forum/development/8624646?cid=8627638
    -- ...

Тогда ad-hoc полиморфные функции - просто «умные аксессоры»:

sup : {A B : Set} ⦃ A≤B : A ≤ B ⦄ → A → B
sup ⦃ instance ⦄ = _≤_.sup instance

sub : {A B : Set} ⦃ A≤B : A ≤ B ⦄ → B → Maybe A
sub ⦃ instance ⦄ = _≤_.sub instance

то есть

sup :: SubtypeRelation a b -> a -> b
sup = _sup

sub :: SubtypeRelation a b -> b -> Maybe a
sub = _sub

отличие в том, что запись должна передаваться неявно, то есть если мы используем (sup (5 :: Int)) :: Integer, то есть тип тут Int ≤ Integer, то нужно неявно найти определение записи с таким типом, вроде

intSupInteger :: SubtypeRelation Int Integer
intSupInteger = SubtypeRelation {
  _sup = \x -> toInteger x,
  _sub = \x -> if x < toInteger (maxBound :: Int) && x > toInteger (minBound :: Int) then Just (fromInteger x) else Nothing
  }

убедиться что такое определение одно (нет конфликтов) и дальше неявно передавать эту запись (вытаскивать из неё реализации sup/sub, подставлять, etc).

Ещё можно передать такую запись явно, просто как аргумент:

test : SuperType
test = sup ⦃ A≤B = someInstance ⦄ sub-value

Наконец, при таком подходе фантомность таких интерфейсов это та же обычная фантомность что и для типов - можно написать

data SomeRep : Set where
  -- ...

record _≤_[_] (A B : Set) (rep : SomeRep) : Set where
  field
    sup : A → B
    sub : B → Maybe A

sup : {A B : Set} {rep : SomeRep} ⦃ A≤B : A ≤ B [ rep ] ⦄ → A → B
sup ⦃ instance ⦄ = _≤_[_].sup instance

sub : {A B : Set} {rep : SomeRep} ⦃ A≤B : A ≤ B [ rep ] ⦄ → B → Maybe A
sub ⦃ instance ⦄ = _≤_[_].sub instance

тип _≤_[_] параметризирован значениями типа SomeRep. sup/sub принимает неявно типы (параметрический полиморфизм), значения типа SomeRep с последующей зависимостью на них (фантомность), инстансы (значения) интерфейса (типа) _≤_[_] (ad-hoc полиморфизм).

То есть параметрический полиморфизм это частный случай функций с pi-types когда типы передаются аргументами - неявность это уже деталь реализации (можно типы параметрически полиморфных функции стереть, в том числе, то есть произвести специализацию), фантомность в частности и proof carrying вообще - тоже, когда передаются значения как свидетели разных утверждений как достаточно богатых типов, ad-hoc полиморфизм - ещё более частный случай передачи инстансов как обычных значений типа/класса-записи (опять с деталями реализации - неявность, поиск в контексте, специализация интерфейса по типу, подстановка функций).

Разное «наследование» и т.п. для интерфейсов - параметризация, агрегация, типы как поля с зависимостями на них.

Это тайпкласс над тремя типами так ?

Да. Это уже тонкости синтаксиса с TypeOperators.

Это наш фантомный тайпкласс

Реальный - (:<), фантомный - (:<:), а это - даже не знаю, скорее просто отношение-предикат на типах, то есть :@ это отношение инстанцирования - Int :@ Num, например, но так не получится, поэтому делаем тип для Num - NumC и пишем инстанс Int :@ NumC - получается населённая истина, но при этом String :@ NumC пока что всё ещё ложь.

И это единственный его инстанс так ?

Это единственный инстанс (:<) - для всех типов a и типов представлений классов cls которые связаны отношением инстанцирования a :@ cls и для всех инстансов (a :<: b) cls фантомного класса (:<:) с любым b и данными a и cls (то есть второй constraint _зависит_ от первого) можно автоматически вывести инстанс (:<).

А есть список статей «для начинающих продвинутых» ?

Не знаю, тут довольно технический момент - трюк в том, чтобы работать с различимыми с точки зрения GHC инстансами класса (:<:) а потом с помощью отношения :@ и зависимости constraintов получить более удобный (:<) для интерфейса.

А так лучше грести в сторону зависимых записей - с ними всё понятно, по крайней мере. И это дальнейшее развитие классического «интерфейс как структура из сигнатур».

Ну это же классическая динамическая типизация с полностью ручной проверкой типов там где необходимо ?

Там есть cast :: (Typeable a, Typeable b) -> a -> Maybe b - оно написано как сравнение typeid который дают инстансы Typeable с последующим Just . unsafeCoerce (мы знаем, что типы «равны»), либо Nothing (не знаем). Ты делаешь примерно то же самое, только реализуешь уже не typeid, а предпорядок в виде отношения (:<) на типах (что, конечно, уже лучше) и sup/sub которые тоже работают во время выполнения.

quasimoto ★★★★
()
Ответ на: комментарий от s9gf4ult

это лишь ограничение GHC, а на самом деле никаких логических противоречий в наличии такой возможности нет, верно ?

Ну, если написать

IntegralInstance :
  {A : Set} {n : ℕ} ⦃ multipable-iface : Multipable n ⦄ ⦃ integral-iface : Integral A ⦄
  → Multiple n A ≤ A
IntegralInstance {n = n} ⦃ multipable-iface = mi ⦄ ⦃ integral-iface = if ⦄ = record
  { sup = {!!}
  ; sub = {!!}
  }

RealFracInstance :
  {A : Set} {n : ℕ} ⦃ multipable-iface : Multipable n ⦄ ⦃ realfrac-iface : RealFrac A ⦄
  → Multiple n A ≤ A
RealFracInstance {n = n} ⦃ multipable-iface = mi ⦄ ⦃ realfrac-iface = rfi ⦄ = record
  { sup = {!!}
  ; sub = {!!}
  }

то при использовании, например, Multiple 2 Int ≤ Int можно найти в контексте A ~ Int, n ~ 2, Multiple 2 Int ≤ Int, Multipable 2 и Integral Int и не найти того же для RealFracInstance - всё получается хорошо. Агда сейчас так не умеет, точнее, она делает именно так, но плоско, то есть для простых инстансов - я могу написать в глобальный контекст int-is-num : Num Int и оно будет неявно подбираться, потом написать int-is-num-again : Num Int и будет ошибка, так что нужно явно указать какой инстанс я хочу (и тут ошибок уже не будет - может существовать сколько угодно перекрывающихся инстансов если они используются явно, конфликты нужно разрешать при неявном использовании). Если сделать более глубокий поиск - твой пример будет работать до тех пор пока точно так же можно найти в контексте однозначный (уникальный) набор «инстансов».

quasimoto ★★★★
()
Последнее исправление: quasimoto (всего исправлений: 1)
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.