LINUX.ORG.RU

Два подхода к контекстам

 , ,


4

2

В этом посте я собираюсь рассмотреть различия в объектной модели Smalltalk и CLOS и как эти модели связаны с понятием контекста. Поклонники статической типизации могут не читать. С открытием CLOS возникли споры о том, CLOS — это что? ООП или не ООП? Становление новой науки неизбежно приводит к терминологическим спорам. ООП — термин расплывчатый и ИМХО, его следовало бы избегать. Как CLOS, так и Smalltalk реализуют одну важную фичу — ad hoc полиморфизм. Эта фича крайне важна для программирования, т.к. позволяет настраивать уже существующий код без изменения его исходного текста. Модель Smalltalk имеет ограниченный ad hoc полиморфизм, т.к. фактически позволяет производить диспетчеризацию лишь по одному аргументу. Однако, кроме ad hoc полиморфизма есть еще одна вещь, связанная с ООП — инкапсуляция. Итак, кратко опишем две ОО модели:

  • Инкапсуляция и ad-hoc полиморфизм (Smalltalk).
  • Ad-hoc полиморфизм без инкапсуляции (CLOS).

Далее я покажу, что эти два подхода противостоят друг другу. В Smalltalk объект — самодостаточная сущность. Чтобы распечатать объект (получить его внешнюю репрезентацию) необходимо послать объекту соответствующее сообщение. Это означает, что внешняя репрезентация объекта зависит только от него, и в минимальной степени зависит от внешнего контекста вызова. В CLOS внешняя репрезентация объекта целиком и полностью зависит от текущей обобщенной функции print-object. Теоретически у одного экземпляра Lisp системы может быть много различных обобщенных функций print-object.

Обычно естественные языки имеют лишь одно текстовое представление. Это не так для иероглифических языков, и скорее всего мы придём со временем к ситуации, когда один и тот же язык будет иметь множество проекций на текст. К этому же идет и эволюция языков программирования. Так, в Perl 6 функция load принимает на вход грамматику, которая описывает Perl 6 и написана на Perl 6. Далее свойство независимости от внешнего представления мы будем называть синтаксической абстракцией. ЯП, наиболее полно поддерживающий синтаксическую абстракцию — Lisp. Программы на Lisp записываются в виде S-выражений, но скобки тут нужны только для того, чтобы указать ридеру структуру. В Lisp текстовая репрезентация программы называются выражениями, а вычислитель работает не с выражениями, а с формами. Термин форма подчеркивает абстрактную природу вычислителя. Я ранее уже писал о том, как можно усилить синтаксическую абстракцию символов в Lisp.

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

Контекст — крайне важное понятие. Игнорирование контекста и абсолютизация понятий приводит к проблемам. Как определить тип объекта? Широко известен спор между Платоном и Диогеном. «Человек, - сказал Платон, - это двуногое животное без перьев». Тогда Диоген ощипал петуха и со словами: «Вот твой человек». Платону пришлось сделать уточнение: «Двуногое животное без перьев и имеющее ногти». Понятно, что тип объекта зависит от наблюдателя. Языки программирования начали с простой идеи — к каждому объекту прилеплен бейджик с его типом. В Smalltalk человеком является тот, кто на вопрос «ты кто?» отвечает — человек. Какой может быть типизация, которая учитывает контекст? Она носит название предикатная типизация. Еще иногда эту идею называют утиной типизацией. В этом подходе тип объекта зависит от того, кто и какие вопросы задает объекту. Платон может определить человека по отсутствию перьев и наличию ногтей, а Диогену нужен фонарь, чтобы определить, кто есть человек.

Одна из наиболее важных идей в истории программирования — гомоиконность является примером переключения контекста. В Lisp нет специально выделенного типа для AST. Является ли определенное дерево AST или нет, зависит от намерений исполнителя. Благодаря этому стало возможным метапрограммирование без значительного усложнения языка. Язык, который выделяет отдельный тип для AST должен иметь весь набор селекторов и конструкторов для этого типа, тогда как представление AST формами дает возможность пользоваться общими селекторами и конструкторами.

Я не отвергаю полностью концепцию инкапсуляции, но хочу обратить внимание, что ее абсолютизация (как в Smalltalk) противоречит некоторым принципам заложенным в динамические языки.

Ответ на: комментарий от monk

Дальше я написал, что такая функция с одним аргументом принадлежит множеству ≅ State(ℤ, ⊤).

Напиши на C++ class Even { ... void inc() /* тут const нет */ ... } и потом сделай const /* хотел константу */ Even x(3 /* => 6 */) и попробуй вызвать x.inc().

motto
()
Ответ на: комментарий от J-yes-sir

А слабо вот так вот, с сохранением всех цепочек наследования

Не слабо. :-)

#lang racket
(define (pget pobject key)
  (hash-ref (get-field __this__ pobject) 
            key
            (λ () (hash-ref (get-field __proto__ pobject) key))))

(define (psend pobject key . args)
  (apply 
   (hash-ref (get-field __this__ pobject) 
             key
             (λ () (hash-ref (get-field __proto__ pobject) key)))
   pobject
   args))

(define (pset! pobject key value)
  (set-field! __this__ pobject (hash-set (get-field __this__ pobject) key value)))

(define prototype% (class object% (super-new)
                     (field [__proto__ (hash)]
                            [__this__ (hash)])))

(define man%
  (class prototype%
    (inherit-field __proto__)
    (super-new)
    (set! __proto__
          (hash 'legs 2
                'hands 2
                'head 1
                'fullName (λ (this) (string-append (pget this 'name) " " (pget this 'lastName)))))))

(define boy%
  (class man%
    (inherit-field __proto__ __this__)
    (init-field name lastname)
    (super-new)
    (set! __this__
          (hash 'name name
                'lastName lastname))
    (set! __proto__
          (hash-set* __proto__
                     'power 'strong
                     'sex 'male))))

(define girl%
  (class man%
    (inherit-field __proto__ __this__)
    (init-field name lastname)
    (super-new)
    (set! __proto__
          (hash-set* __proto__
                     'power 'weak
                     'sex 'female))
    (set! __this__
          (hash 'name name
                'lastName lastname))))

(define boy (make-object boy% "Jack" "Smith"))

(displayln (~v (pget boy 'hands)
               (pget boy 'legs)
               (pget boy 'head)
               (psend boy 'fullName)
               (pget boy 'power)
               (pget boy 'sex)))

(pset! boy 'name "Jane")
(pset! boy 'sex 'female)

(set-field! __proto__ boy (get-field __proto__ (make-object girl% "" "")))

(displayln (~v (pget boy 'hands)
               (pget boy 'legs)
               (pget boy 'head)
               (psend boy 'fullName)
               (pget boy 'power)
               (pget boy 'sex)))

;;; выводит
2 2 1 "Jack Smith" 'strong 'male
2 2 1 "Jane Smith" 'weak 'female
monk ★★★★★
()
Ответ на: комментарий от qweqwe

контракты навешаны, и мне понадобилась эта функция со сменой класса

Вообще-то это уже противоречащие утверждения.

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

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

и попробуй вызвать x.inc().

error: passing ‘const Even’ as ‘this’ argument of ‘int B::inc()’ discards qualifiers

И что? Я же про то, что числа по сути своей неизменяемые объекты. У эллипса может поменяться длина осей, а у числа что? Ничего. Поэтому «не const» методы для чисел смысла не имеют.

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

или изначально боксить все объекты в ожидании возможной необходимости этой функции

CLOS такой тормозной именно из-за того, что все объекты там всегда боксятся «в ожидании возможной необходимости этой функции». Ну и непрямой вызов методов тоже скорости не добавляет (причём дважды непрямой 1) поиск функции по символу 2) поиск метода по функции).

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

А вот если я, например расширю родительские классы уже после переопределения объекта, наследование подхватится в этом варианте?


Man=function(){} 
Man.prototype.legs=2
Man.prototype.hands=2
Man.prototype.head=1
Man.prototype.fullName=function(){return this.name+" "+this.lastName}

Girl=function(name, lastname){
this.name=name
this.lastName=lastname
}
Girl.prototype=Object.create(Man.prototype)
Girl.prototype.power="week"
Girl.prototype.sex="female"

Boy=function(name, lastname){
this.name=name
this.lastName=lastname
}
Boy.prototype=Object.create(Man.prototype)
Boy.prototype.power="strong"
Boy.prototype.sex="male"

boy=new Boy("Jack", "Smith")

boy.name="Jane"
boy.__proto__=Girl.prototype

Man.prototype.heart="warm" // хотелось бы в это верить:)
Girl.prototype.tits=true


console.log(
boy.hands,
boy.legs,
boy.head,
boy.fullName(),
boy.power,
boy.sex,
boy.tits,
boy.heart
)

J-yes-sir
()
Ответ на: комментарий от J-yes-sir

вывод забыл скопировать

//  2 2 1 'Jane Smith' 'week' 'female' true 'warm'
//fixed

J-yes-sir
()
Ответ на: комментарий от monk

Вообще-то это уже противоречащие утверждения.

Ну почему же. Может я плохо выразился. Вот имеем такую функцию

(define/contract (marry! boy girl)
  (-> (is-a?/c boy%) (is-a?/c girl%) void?)
  (set-field! spouse boy girl)
  (set-field! spouse girl boy))
потом вдруг оказалось что нужно предусмотреть возможность смены пола и производить нудные, бойлерплейтные трансформации типа

(define/contract (marry! boy girl)
  (-> (box/c (is-a?/c boy%)) (box/c (is-a?/c girl%)) void?)
  (set-field! spouse (unbox boy) (unbox girl))
  (set-field! spouse (unbox girl) (unbox boy)))
qweqwe
()
Ответ на: комментарий от qweqwe

потом вдруг оказалось что нужно предусмотреть возможность смены пола

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

(define-syntax-rule (&set-field! f obj val)
   (set-field! f (unbox obj) val))

(define-syntax-rule (&get-field! f obj val)
   (get-field! f (unbox obj)))

(define-syntax-rule (&send obj args ...)
   (send (unbox obj) args ...))

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

Так всех проблем не решить. К тому же такой способ приведет к созданию своего надмножества к racket/class, когда можно было бы просто добавить рефлексии в изначальную модель и решить через простое (set! this% other%). Но там похоже утинную типизацию уже не прикрутить.

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

Even < Integer ничем не отличается от Circle < Ellipse — если объект Ellipse представляет из себя не просто эллипс-значение, то есть не 2-вектор, такой же чистый как твоё число (скаляр — 1-вектор, btw), но имеет состояние в виде своих длин которые можно изменять (Ellipse -> Ellipse в рамках State), то объект Integer тоже есть переменная, а не просто чистое число, он имеет состояние в виде числового значения, которое тоже можно изменять (Integer -> Integer в State). Если ты говоришь const Circle x(5), то это просто значение, 2-вектор (если Circle чисто интерфейсный наследник Ellipse с парой чисел), проблемы нет, Circle x(5) — уже объект с состоянием принимающим разные значения 2-векторов, изменяемый с помощью Circle -> Circle/Ellipse, есть проблема; const Even x(5) — значение, число, нет проблемы, Even x(5) — объект с состоянием с разными численными значениями изменяемыми Even -> Even/Integer, есть проблема. Пусть class Integer помещается в регистр (в C++ значение так и называется — «скаляр»), тогда class Even : Integer тоже помещается в регистр, const Even это число-значение, Even — регистр с состоянием, тот же, что у Integer, так что тип Even не ловит родительскую мутабельность этого регистра вида Even -> Integer (вот state monad — ловит, потому что тупая как пробка).

Если твои числа, т.е. 1-вектора, неприкосновенные, то проблем нет. Ну так и у 2-векторов тоже нет. Если ты берёшь состояния с разными значениями в виде 2-векторов и мы начинаем обсуждать «проблему эллипса» (теориямножествненеслышали), то и с 1-векторами, т.е. числовыми переменными, то же самое будет.

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

Короче, пусть будет отрезок с направлением в дополнение к эллипсу — можно масштабировать круг в эллипс, можно нарастить складываемый пополам (нацело) отрезок в нескладываемый.

motto
()
Ответ на: комментарий от J-yes-sir

А вот если я, например расширю родительские классы уже после переопределения объекта, наследование подхватится в этом варианте?

В той версии «расширю родительские классы» не существует — хэши иммутабельные. Но вообще можно:

#lang racket

(define prototype (make-hash))

(define (pget pobject key)
  (hash-ref (get-field __this__ pobject) 
            key
            (λ ()
              (for/or ([h (in-list (get-field __proto__ pobject))])
                (hash-ref h key #f)))))

(define (psend pobject key . args)
  (apply (pget pobject key) pobject args))

(define (pset! pobject key value)
  (hash-set! (get-field __this__ pobject) key value))

(define prototype% (class object% (super-new)
                     (field [__proto__ (make-hash)]
                            [__this__ (make-hash)])))

(define-syntax-rule (proto-class super (proto-fields ...) (this-fields ...) rest ...)
  (let* ([proto (cons
                 (hash-copy (hash proto-fields ...))
                 (hash-ref prototype super null))]
         [res (class super
               (inherit-field __proto__ __this__)
               rest ...
               (super-new)
               (set! __proto__ proto)
               (set! __this__
                     (hash-copy
                      (hash this-fields ...))))])
    (hash-set! prototype res proto)
    res))

(define man%
  (proto-class prototype%               
               ('legs 2
                'hands 2
                'head 1
                'fullName (λ (this) (string-append (pget this 'name) 
                                                   " " 
                                                   (pget this 'lastName))))
               ()))

(define boy%
  (proto-class man%
               ('power 'strong
                'sex 'male)
               ('name name
                'lastName lastname)
               (init-field name lastname)))

(define girl%
  (proto-class man%
               ('power 'weak
                'sex 'female)
               ('name name
                'lastName lastname)
               (init-field name lastname)))

(define boy (make-object boy% "Jack" "Smith"))

(pset! boy 'name "Jane")
(pset! boy 'sex 'female)

(set-field! __proto__ boy (hash-ref prototype girl%))

(hash-set! (car (hash-ref prototype man%)) 'heart 'warm)
(hash-set! (car (hash-ref prototype girl%)) 'tits #t)

(displayln (~v (pget boy 'hands)
               (pget boy 'legs)
               (pget boy 'head)
               (psend boy 'fullName)
               (pget boy 'power)
               (pget boy 'sex)
               (pget boy 'tits)
               (pget boy 'heart)))

;;; выводит
2 2 1 "Jane Smith" 'weak 'female #t 'warm

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

Интересно, а аналог CLOS для тикля есть?

XOTcl, возможно.

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

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

J-yes-sir
()
Ответ на: комментарий от motto

Если твои числа, т.е. 1-вектора, неприкосновенные, то проблем нет. Ну так и у 2-векторов тоже нет.

Теперь понял про что речь. Инерция мышления: число вводится как число (а значит его изменить нельзя), а эллипс как структура (а значит изменяемый). Если «числом» считать как в Java класс Integer  — объект с одним полем и разрешить его менять методами, то да — все проблемы аналогичные «эллипсу» вылезут.

monk ★★★★★
()
Ответ на: комментарий от J-yes-sir

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

Весь код, описывающий прототипные классы — кусок до «define man%». Он универсален в том смысле, что на его основании можно описывать произвольные классы.

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

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

Эллипсы изоморфны комплексным _числам_ же

Два разных эллипса могут иметь одинаковые оси. А если два числа равны, то это одно и то же число. Поэтому эллипсы обычно (бывает Haskell) изменяемые, а числа обычно (бывает Fortran) нет.

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

когда можно было бы просто добавить рефлексии в изначальную модель и решить через простое (set! this% other%)

Нельзя. Техническая проблема: что делать с полями и инициализацией.

Идеологическая проблема: если у меня есть контейнер (например, список) и при создании он прошёл проверку по контракту, то я ожидаю, что элементы этого контейнера самопроизвольно меняться не будут. Может меняться только содержимое элементов, но не их тип.

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

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

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

Два разных эллипса могут иметь одинаковые оси.

Не могут, diag(a, b) exp(i phi) это всегда один эллипс (не всем комплексным, а с Re/Im >= 0).

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

Не могут, diag(a, b) exp(i phi) это всегда один эллипс

Нарисуем на листе два эллипса с малой осью — 1см и большой — 2см. Они разные. один выше на листе, другой — ниже. Один красный, другой — зелёный. :-)

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

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

Эллипс с положением это уже четвёрка чисел — R, R, R+, R+, углом наклона — ещё одно [0, pi/2), ориентацией — булево значение, цветом — ещё одно число :)

Дальше для подходящей группы G можно построить классы эквивалентности по отношению z ~ g z' for some g in G, устраняя какие-то различия — цветов и ориентации циклическими группами, положений группой трансляций, углов — вращений [0, pi/2), порядка осей — вращений на pi/2 (или масштабирований diag(b/a, a/b)), в итоге у нас останется эллипс как геометрическая фигура без каких-то внешних степеней свободы, чистая форма которая определяется парой a >= 0, b >= 0, a >= b, дальше остаётся только группа масштабирований и единственный топологический класс окружности — в таком смысле все эллипсы (топологически) эквивалентны.

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

Техническая проблема: что делать с полями и инициализацией.

Если есть общий суперкласс, то, его поля копируются по умолчанию, и если надо изменяются явно (как в struct-copy). Отсутсвующие в изменнем классе удаляются, отсутсвующие в изменяемом принимают дефолтное значение.

Что-то вроде

(define human%
  (class object%
    (super-new)

    (field [age 'uninitialized]
           [favourite-color 'uninitialized]
           [name 'uninitialized])

    (abstract opposite-sex%)

    (define/public (init-human! name-v age-v favourite-color-v)
      (set! name name-v)
      (set! age age)
      (set! favourite-color favourite-color-v))

    (define/public (introduce)
      (format "~a: ~a, ~a, ~a" this% name, age, favourite-color))
    
    (define/public (change-sex! new-name)
      (change-class! (opposite-sex%) [name new-name]))    
    ))


(define boy%
  (class human%
    (super-new)

    (field [gay? 'uninitialized])
    
    (inherit init-human!)
    
    (define/override (opposite-sex%) girl%)

    (define/public (init-boy! gay?-v . rest)
      (set! gay? gay?-v)
      (apply init-human! rest))

    (define/override (introduce)
      (format "~a and I am ~a gay" (super introduce) (if gay? "" "not")))
    
    ))


(define girl%
  (class human%
    (super-new)

    (field [size 'uninitialized])
    
    (inherit init-human!)
    
    (define/override (opposite-sex%) boy%)

    (define/public (init-size! v)
      (set! size v))

    (define/public (init-girl! size-v . rest)
      (init-size! v)
      (apply init-human! rest))

    (define/override (introduce)
      (format "~a and my size is ~a" (super introduce) size))
    
    ))


(define person (new boy%))
(send* person
  (init-boy! #t "John" 32 'pink)
  (introduce))
;; =>
;; boy%: John, 32, pink and I am gay
(send* person
  (change-sex! "Jane")
  (init-size! 2)
  (introduce))
;; =>
;; girl%: Jane, 32, pink and my size is 2

Идеологическая проблема: если у меня есть контейнер (например, список) и при создании он прошёл проверку по контракту, то я ожидаю, что элементы этого контейнера самопроизвольно меняться не будут. Может меняться только содержимое элементов, но не их тип.

Ну тип(класс) в общем-то ведь и есть поле. Не вижу проблем тут.

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

Так контракты и проверяют каждый раз. Разве нет? Мне место с описанием контрактов, которые проверяются не сразу и не всегда не очень понятно.

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

Почему? Вроде нормально упадет. Факт мутирования объекта обнаружится на первом контракте после смены класса. Ну то есть, все так же как с контрактами на любые другие мутабельные структуры должно быть.

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

Вот так не пишут:

...
(define person (new boy%))
(send* person
  (init-boy! #t "John" 32 'pink)
...

а пишут так:

(define person (new boy% [gay #t] [name "John"] [age 32] [favourite-color 'pink]))

И в коде вполне может быть что-то вроде

(define girl%
  (class human%
     (super-new)
     (init-field tits)
     (init dress)
     (write-to-file "new girl" name)
     (define mature (> age 18))
     ...
     (define (change-age new-age)
        (set! age new-age)
        (when (> age 18) (set! mature #t)))
     ...))

Должны ли при смене класса отрабатывать строки с записью в файл? Чем заполнятся init-параметры?

Факт мутирования объекта обнаружится на первом контракте после смены класса.

Если при вызове функции выполняется (is-a?/c boy%) или даже (list/c (is-a?/c boy%)), то можно гарантировать, что до выхода из функции этот факт не изменится. В твоём варианте необходимо проверять данные после вызова любой внешней функции. Получается программирование как будто все переменные глобальные. Это ужасно.

Ну и если тебе это действительно надо, то wrapper через racket/class и box пишется за полчаса. В смысле, делаешь модуль, копирующий API racket/class, но при создании объекта заворачивающий его в box, а при использовании (в аргументе send и т.д.) в unbox.

После этого используешь везде свой модуль вместо racket/class.

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

в таком смысле все эллипсы (топологически) эквивалентны.

... а значит все комплексные числа эквивалентны единице. :-)

Смысл-то вроде не в этом.

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

Эта уже присказка, а не какой-то относящийся к делу вывод — вряд ли нам нужна такая эквивалентность. В остальном геометрическая фигура, даже с положением и цветом, ничем не хуже числа — вот она есть такая как есть и всё. Семантику значений (чисел, фигур) тогда изучает обычная математика, а семантику изменяемых состояний в ЯП изучает теория ЯП (TAPL 13 References, PFPL 36 Assignable References, SF References, например) — известно, что ссылки и массивы инвариантны (ковариантны по чтению и контравариантны по записи), в Java массивы ковариантны — fail, наследование с контравариантностью изменения состояния методами предка — fail (оно и не обязано, наследование не образует подтипов и все гарантии типа SOLID нужно соблюдать самим).

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

В остальном геометрическая фигура, даже с положением и цветом, ничем не хуже числа — вот она есть такая как есть и всё.

Фигура может меняться во времени. Например, программа отображает несколько соударяющихся эллипсов. Необходимо отслеживать идентичность каждого эллипса (а не приравнивать эллипс к его состоянию). Число же измениться не может (но может измениться любая величина, измеряемая в числах).

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

(ковариантны по чтению и контравариантны по записи)

Вот это и есть основная головная боль ООП. Решается тремя путями:

1) запрет состояний в классах (например, http://common-lisp.net/~frideau/lil-ilc2012/lil-ilc2012.html)

2) запрет наследования произвольных подтипов (остаются только интерфейсы и SOLID)

3) класс объекта не является постоянным (мой GLS).

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

Вот так не пишут:

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

Должны ли при смене класса отрабатывать строки с записью в файл? Чем заполнятся init-параметры?

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

Если при вызове функции выполняется (is-a?/c boy%) или даже (list/c (is-a?/c boy%)), то можно гарантировать, что до выхода из функции этот факт не изменится.

Почему это? Нельзя этого гарантировать. Для таких гарантий нужно объявлять post-condition в контракте.

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

Гм, вот такой пример. Некий ресторан обслуживает только мужчин в костюмах. Мужик в костюме заходит в ресторан (проходит контракт) и сразу же раздевается или меняет пол. На функцию взять заказ у официанта тоже стоит контракт на мужика в костюме. То есть, официант подходит к этому голому/сменившему пол и заявляет о нарушении контракта. Ничего дополнительно проверять не нужно, разницы между изменением поля и класса по сути нет. Примерно так:

(define freak (new man% [clothes 'suit]))

(define/contract (satisfies-dresscode? man)
  (-> (is-a?/c man%) boolean?)
  (eq? (get-field clothes man) 'suit))

(define/contract (serve! man)
  (-> (and/c (is-a?/c man%) satisfies-dresscode?) void?)  
  (set-field! clothes man 'naked)
  ;; (send man change-sex!)
  (define request (take-request man))
  'rest-service
  (void))

(define/contract (take-request man)
  (-> (and/c (is-a?/c man%) satisfies-dresscode?) symbol?)
  'request)

(serve! freak)

Ну и если тебе это действительно надо, то wrapper через racket/class и box пишется за полчаса. В смысле, делаешь модуль, копирующий API racket/class, но при создании объекта заворачивающий его в box, а при использовании (в аргументе send и т.д.) в unbox.

После этого используешь везде свой модуль вместо racket/class.

Так будет грязно. Боксы все равно будут торчать из контрактов, из методов и полей отнаследованных через inherit, inherit-field, да и много еще откуда они будут торчать. Одной волшебной сменой racket/class не обойтись, а если и можно бы было, то все равно костыль рано или поздно о себе напомнит.

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

Фигура может меняться во времени.

http://en.wikipedia.org/wiki/Geometric_shape

A geometric shape is the geometric information which remains when location, scale, orientation and reflection are removed from the description of a geometric object.

Например, программа отображает несколько соударяющихся эллипсов.

Если такой соударяющийся объект перемещается и как-то меняет свою форму (оставаясь эллипсом), то он (его состояние) именно что измеряется эллипсами (форма), положением своего центра масс (2-вектор) и вращательной степенью свободы (угол). Так же отрезок измеряется числом обозначающим его длину, положением центра и углом (всё может меняться при эволюции состояний во времени). Если у нас есть «квадрат со стороной 5», то это конкретная фигура (безотносительно внешних степеней свободы она однозначно задаётся своей группой симметрии, потом дополняется масштабом, положением, ориентаций и т.д., если она будет меняться, то и группа будет меняться, что как-то не очень), «прямоугольник» это уже другая фигура, форму меняют тогда не они, а (не абсолютно твёрдые) тела измеряемые формой в т.ч. (у АТТ только положение и углы, форма фиксирована и не имеет степени свободы).

http://en.wikipedia.org/wiki/Degrees_of_freedom_(mechanics)

http://en.wikipedia.org/wiki/Configuration_space

5 степеней свободы — положение на плоскости, оси эллипса, угол. Конкретное состояние есть конкретные положение, эллипс и угол (точнее это положение в обобщённых координатах, для состояния могут быть нужны ещё их производные по времени), они меняются с эволюцией тела во времени (так что мы с телом связываем объект с состоянием и реализуем некоторую функцию эволюции (Object, ...) -> Object, мутабельно или нет).

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

Число же измениться не может

Геометрическая фигура тоже. Если 3 это 3, то и «окружность радиусом 3» это «окружность радиусом 3» (изометрически), «окружность» — «окружность» (геометрически, масштабирование по всем осям одновременно добавили к группе изометрии), «эллипс» — «окружность», ... (топологически).

Меняться может только физическое тело, его модель (объект с состоянием, например) — у них есть некоторое количество степеней свободы, вот они и меняются.

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

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

То есть это уже не racket/class. А в лучшем случае его подмножество.

Поэтому нормальным поведением, наверное, было бы игнорирование этих init'ов и других вычислений.

Тогда получишь неинициализированные (а значит вообще отсутствующие, так как define не отработал) поля в классе после изменения. Правильный ответ: надо заимствовать shared-initialize + initialize-instance из CLOS, но это уже будет совсем не racket/class.

Почему это? Нельзя этого гарантировать. (про (list/c (is-a?/c boy%)))

Можно: list неизменяем, тип класса — тоже.

разницы между изменением поля и класса по сути нет

Здесь ожно провести аналогию со списками и массивами. Элементы массива всегда изменяемы, элементы списка, как правило — нет. Так как если передаёшь значение переменной в функцию, то не ожидаешь изменения значения переменной. Если доводить идею с change-class до логического конца, то должно быть можно сделать

(define (set-to-3! x)
  (change-class! x <integer>)
  (set-field! value x 3))
(define x "ok")
(set-to-3! x)
x ;; здесь 3
но это уже полностью противоречит семантике лиспа

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

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

Или бери мой protoclass и делай всё, что можно сделать в JS включая изменение полей и методов у класса и изменение класса у объекта.

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

Меняться может только физическое тело

Я под эллипсом подразумевал именно физическое тело (в графическом редакторе например). Для математического эллипса действие «изменить ось» действительно неприменимо, так как эллипс лишь описание состояния.

monk ★★★★★
()
Последнее исправление: monk (всего исправлений: 1)
Ответ на: комментарий от monk

Тогда получишь неинициализированные (а значит вообще отсутствующие, так как define не отработал) поля в классе после изменения

Нет, они по идее должны были инициализироваться в мутируемом классе если неявная инициализация все же присутсвует. Если это не так, то такая инициализация была плохо спроектирована.

Если доводить идею с change-class до логического конца, то должно быть можно сделать ... но это уже полностью противоречит семантике лиспа

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

почему это?

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

Или бери мой protoclass

что-то он ужасно выглядит :)

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

Если это не так, то такая инициализация была плохо спроектирована.

Кгм. Вообще-то есть уйма объектов, не имеющих смысле без заданных параметров инициализации. Например, для кнопки из racket/gui обязательно требуется объект, в котором она находится.

Я понимаю, что ты имеешь в виду семантику CLOS, где все поля имеют значение по-умолчанию и инициализация отделена от класса. Но семантика racket/class принципиально другая, ближе к типизированным языкам.

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

В таком случае лучше не делать классы мальчик/девочка, а добавить вариативность действий в класс человек в зависимости от поля «пол». В Racket крайне неохотно позволяют в стандартной библиотеке действия, которые легко случайно использовать неверно. А change-class неверно использовать едва ли не проще, чем rplacd

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

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

Зря. Racket это не CL и у require есть замечательный параметр prefix-in. Что позволяет использовать в одном проекте несколько модулей с идентичным API не особо напрягаясь. Как пример make из web-server/dispatchers/*.

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

В таком случае лучше не делать классы мальчик/девочка, а добавить вариативность действий в класс человек в зависимости от поля «пол»

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

А в идеале хотелось бы даже не класс менять, а возможность удалять и добавлять вершины со всеми зависимостями в дереве наследования

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

А в идеале хотелось бы даже не класс менять, а возможность удалять и добавлять вершины со всеми зависимостями в дереве наследования

Тогда тебе точно брать в качестве движка что-то вроде protoclass и ваять свой ООП. Там-то как раз можно: (hash-ref prototype <имя класса>) читает дерево наследования, hash-set! — меняет. API можно сделать более красивым — для того макросы и придуманы.

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

Тогда тебе точно брать в качестве движка что-то вроде protoclass и ваять свой ООП

Тоже подумал об этом. Мне как раз еще некоторых штук не хватает в racket/class.

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

Even < Integer ничем не отличается от Circle < Ellipse — если объект Ellipse представляет из себя не просто эллипс-значение, то есть не 2-вектор, такой же чистый как твоё число (скаляр — 1-вектор, btw), но имеет состояние в виде своих длин которые можно изменять (Ellipse -> Ellipse в рамках State)

Ну и изменяй, какие проблемы? Ничего не сломается, гарантированно.

то объект Integer тоже есть переменная, а не просто чистое число, он имеет состояние в виде числового значения, которое тоже можно изменять (Integer -> Integer в State).

и также меняешь интеджеры свои.

Even x(5) — объект с состоянием с разными численными значениями изменяемыми Even -> Even/Integer, есть проблема.

Где проблема?

регистр с состоянием, тот же, что у Integer, так что тип Even не ловит родительскую мутабельность этого регистра вида Even -> Integer

Почему это не ловит? У even просто нету метода, который сможет сломать even и сделать его integer. Нельзя написать такой метод. Так же как для circle невозможно написать метод, который сделает circle ellips'ом. Вообще как можно из circle сделать ellipse, если у ellipse есть длина и ширина, а у circle - только радиус? Для этого надо как-то взять и добавить к классу дополнительные поля, в ООП это вообще говоря невозможная операция (если мы только не говорим о суровой динамике с рефлексиями и т.п., но это уже к разговору не должно относиться). Какое-то по определению бессмысленное мероприятие.

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

На самом деле неизоморфны, очень легко это доказывается.

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

Ты путаешь идентичность объектов с их эквивалентностью. Разные объекты могут быть эквивалентны, но не могут быть идентичны (по определению).

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

http://en.wikipedia.org/wiki/Circle-ellipse_problem

На самом деле неизоморфны, очень легко это доказывается.

Я знаю, как множества — пары a, b >= 0 и комплексные с Re и Im >= 0 (с-но матрицы масштабирования diag(a, b) которыми можно превращать окружность exp(i phi) в комплексной плоскости в эллипсы), иначе нужны «эллипсы со спином 3/2», чтобы с ними как с комплексными числами работать (складывать, умножать, что всё равно скорее всего искусственно и не нужно).

Ты путаешь идентичность объектов с их эквивалентностью.

Как я их путаю, если про идентичность ничего не говорю, только про эквивалентность?

Пусть есть struct Ellipse { PositiveScalar a, b; } и struct EllipseWithPosition { Ellipse ellipse; Scalar x, y; } — первое вкладывается во второе (как подобъект), второе проецируется на первое. Есть серьёзные equalityOnEllipse и equalityOnEllipseWithPosition, есть equivalenceOnEllipseWithPositionModuloTranslations (формально через equalityOnEllipseWithPosition и трансляции, e ~ T e' если существует T) которое как функция равна equalityOnEllipse, так что можно вместо него спроецировать («забыть») и сравнить — выбор эквивалентности может быть выбором конкретной структуры с конкретной идентичностью.

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

Я знаю, как

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

Как я их путаю, если про идентичность ничего не говорю, только про эквивалентность?

Речь шла как раз про идентичность, а ты спутал ее с эквивалентностью.

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

http://en.wikipedia.org/wiki/Circle-ellipse_problem

Это проблема из разряда «мы можем написать функцию сортировки, которая не сортирует». Да, мы можем назвать «Circle» класс, который не будет вести себя как circle. И дальше что? :)

Главное, что мы _можем_ реализовать ф-ю сортировки, которая будет сортировать и _можем_ сделать Circle, который будет подтип Ellipse и вести себя в точности как должен вести circle (и ellipse).

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

в ооп с мутабельностью только один изоморфизм

Тебя понесло, речь шла про 1) эллипсы как значения, 2) мутабельные объекты со значениями эллипсов, т.е. состояние — такой-то эллипс. А ещё без жоп, да, так что 1) как чистые математические эллипсы.

1) Множество (и структура масштабирования) эллипсов изоморфно диагональным 2x2 матрицам масштабирования, ну или определённым парам, комплексные числа (со структурой) изоморфны определённым 2x2 матрицам или парам с определённой структурой. Так что мораль — комплексные числа, матрицы, эллипсы такие же «числа» как и все прочие, тем более, что «прочие» не так уж просты — модели вещественных как классов эквивалентности последовательностей, рациональных как классов пар целых, целых как классов пар натуральных, натуральных как простейших связных списков.

2) ооп, мутабельностью и т.п. отдельно обсудили.

Речь шла как раз про идентичность, а ты спутал ее с эквивалентностью.

(identity, equality, equivalence — первое вообще хрен пойми что, но путать его нужно с известным понятием из математики? пусть идентичность = equality)

Нет, речь шла про эллипс — натурально struct Ellipse со своей идентичность, http://en.wikipedia.org/wiki/Geometric_shape. Потом monk стал говорить об эллипсе (ещё раз — isometric/geometric/topological shape) с положением (и цветом), это уже struct EllipseWithPosition (и цвет!) с другой идентичностью и эквивалентностью восстанавливающей struct Ellipse, поэтому я заговорил про группу восстанавливающую фигуры, http://en.wikipedia.org/wiki/Euclidean_group, http://en.wikipedia.org/wiki/Scaling_(geometry).

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

Тебя понесло

Это тебя понесло. Еще раз - в мутабельном ООП есть только _один_ изоморфизм. По-этому нельзя реализовать класс комплексных чисел и класс эллипсов так, чтобы они оказались изоморфными. Конец истории. Математические эллипсы, множества ,матрицы и прочая херь - просто не имеют абсолютно никакого отношения к разговору. Это объекты, о которых речь и близко не шла.

Нет, речь шла про эллипс — натурально struct Ellipse со своей идентичность

Речь шла об эллипсе в контексте реализации класса Ellipse в ООП. Значит - об идентичности. А ты занялся подменой понятий.

(identity, equality, equivalence — первое вообще хрен пойми что, но путать его нужно с известным понятием из математики? пусть идентичность = equality)

Формально, идентичность - минимальное рефлексивное отношение.

anonymous
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.