Форум программистов «Весельчак У»
  *
Добро пожаловать, Гость. Пожалуйста, войдите или зарегистрируйтесь.
Вам не пришло письмо с кодом активации?

  • Рекомендуем проверить настройки временной зоны в вашем профиле (страница "Внешний вид форума", пункт "Часовой пояс:").
  • У нас больше нет рассылок. Если вам приходят письма от наших бывших рассылок mail.ru и subscribe.ru, то знайте, что это не мы рассылаем.
   Начало  
Наши сайты
Помощь Поиск Календарь Почта Войти Регистрация  
 
Страниц: [1]   Вниз
  Печать  
Автор Тема: Размышления об идеальном языке программирования: Типы  (Прочитано 1465 раз)
0 Пользователей и 1 Гость смотрят эту тему.
Dimka
Деятель
Модератор

ru
Offline Offline
Пол: Мужской

« : 07-01-2014 20:05 » 

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

Традиционным способом ответа на эту сложность является введение системы типов.

Типы в программировании появились с самого начала. На низком уровне тип определяет способ интерпретации совокупности битов (или байтов). Например, в одном случае 4 байта на 32-хразрядной аппаратной платформе могут интерпретироваться как int (целое число со знаком), а в другом как float (вещественное число с плавающей запятой, состоящее из мантиссы и порядка, и каждая часть со своим знаком). В обоих случаях битовые поля внутри 4-хбайтовой структуры различны, и, например, арифметические операции проводят к различному преобразованию битов.

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

Типизированными языками (обычно компилируемыми) или языками со статической типизацией называются такие, в которых каждый идентификатор в программе имеет заданный программистом тип. Как правило с идентификатором ассоциирована некая область памяти под значение, и вся эта совокупность из идентификатора и места в памяти под значение называется переменной. Заранее определённый программистом тип нужен для того, чтобы ещё на этапе компиляции уже было понятно, какие операции и каким именно способом можно выполнять со значением переменной. Это позволяет ещё на этапе разработки отлавливать некоторые ошибки. Зато это не позволяет писать достаточно абстрактный (и потому компактный) программный код.

Нетипизированными языками (обычно интерпретируемыми) или языками с динамической типизацией называются такие, в которых каждый идентификатор в программе не имеет определённого типа и может ссылаться на любое значение. Такие идентификаторы играют лишь роль ссылок на значения. Сами же значения, разумеется, имеют определённый тип. В этом случае переменных как таковых не возникает, на основе старых значений возникают новые (как объекты в иной области памяти), а ссылки лишь переключаются с одного объекта-значения на другой. Объекты, теряющие все ссылки на себя, удаляются механизмом сборки мусора. Совсем бестиповых (нигде не имеющих никаких типов) языков не бывает. В нетипизированных языках все ошибки, связанные с неверной интерпретацией, вылавливаются лишь во время исполнения. И это определяет иной метод разработки, но не является непреодолимым препятствием для крупных проектов. Зато такие языки позволяют писать код с высоким уровнем абстракций (и потому компактный).

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

В языках со статической типизацией АТД реализуются классами: теми же структурами, к которым добавлен ряд процедур, имеющих доступ к устройству структуры, но процедуры вне класса не имеют доступа к устройству структуры. Значением АТД является объект - экземпляр класса, под который, собственно, и отводится память. Например, лампочка:
Код:
class Lamp {
  public enum State { ON, OFF }
  private State state;
  public Lamp() { this.state = State.OFF; }
  public On() { this.state = State.ON; }
  public Off() { this.state = State.OFF; }
  public State View() { return this.state; }
}
Тут задан вспомогательный тип состояний лампочки State, одно внутреннее значение типа State и несколько операций: включить (On), выключить (Off), посмотреть (View). Дальше в программе можно задать несколько значений типа Lamp, и каждое из них будет иметь собственную память под state и собственное значение. Причём пользователю не видно, какие поля описывают состояние лампочки, а также что конкретно делают с данными и этими полями операции над лампочкой.

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

Это способ создания объектов через конструирование. Заключается он в том, что в начале возникает пустой объект, не имеющий в себе ничего. Затем некоторой процедурой (она называется конструктором, но является самой обычной процедурой) в этот объект добавляются его элементы: поля и связанные с объектом операции. В этом случае конкретный тип объекта полностью зависит от процедуры-конструктора. Например, для той же лампочки:
Код:
function Lamp() {
  obj = new object();
  obj.ON = 1;
  obj.OFF = 0;
  obj.state = obj.OFF;
  obj.On = function(this) { this.state = this.ON; }
  obj.Off = function(this) { this.state = this.OFF; }
  obj.View = function(this) { return this.state; }
  return obj;
}
Сконструированный таким способом объект есть объект сам в себе со своими собственными операциями и состояниями. Тут важно, что если мы таким конструктором создадим несколько объектов, каждый из них будет полностью независим в своей жизни от своих братьев. Его конструкция может даже меняться другими функциями: могут добавляться новые поля и операции, удаляться или подменяться имеющиеся. Напомню, что тип нужен в частности для сопоставления объектов. Т.е. только что сконструированные и нереконструированные объекты лампочек будут иметь общий тип, определяемый конструктором. Но в общем случае тип каждого объекта будет определяться всей историей его создания и модификаций - генезисом. В конечном счёте каждый объект свою индивидуальность и идентичность может выражать не только через собственное место в памяти, где он хранится, но и через свой генезис. Тогда объект (как текущее его состояние - значение) и тип объекта (как текущее внутреннее устройство после всех его модификаций) могут в общем-то представлять собой одно и то же.

Возникает вопрос, как же с этим можно работать? Для этого нужны две концепции. Первая концепция - это некая регулярная структура, к которой сводится любой объект. Эта регулярная структура, присущая любому объекту, как бы тот ни был устроен, а также возникающие при этом некоторые одинаковые для всех объектов базовые свойства могут быть выражены в языке программирования в виде метакласса. Такого класса (заведомо неизменного), который обеспечивает единство и регулярность описания любых типов в языке и является основой для сопоставления между собой объектов любых типов. Вторая концепция - это операция "сравнение с образцом" (match with pattern). Именно благодаря регулярной структуре любого объекта любого типа мы можем у любого объекта посмотреть интересующие нас свойства и признаки и на этом основании принять решение, что с объектом можно работать. Обе концепции появились ещё в LISP (где регулярной структурой является список, а сравнение с образцом - вынужденная операция в любом хоть сколь-нибудь сложном проекте). Ныне операция "сравнение с образцом" является одной из базовых в более современных языках функционального программирования. И даже в нефункциональных языках обе эти концепции известны программистам как регулярные выражения (сравнение с образцом) при работе с такими регулярными структурами, как строки. В каком-то смысле и сам код программы, и структуры данных - это тоже некий аналог строк, составленных из более сложных символов, нежели буквы, в котором можно что-искать по заданным шаблонам.

Например, реализуем мы некую процедуру, которая должна включать любые поступающие на вход значения. И мы знаем, что для включения к объекту нужно применить операцию "On". Вот на вход (как значение аргумента) нам приходит объект. Индивидуальный, может быть совершенно уникальный, неизвестно как уродившийся. Нам в общем-то нет никакого дела до его уникальности, нам надо знать: есть с ним связанная операция "On", чтобы его включить, или нет такой операции. Чтобы это выяснить, мы должны выполнить сравнение с образцом - таким особым описанием, в котором задано наличие желаемой операции и задана свобода по всем остальным конструктивным особенностям объекта. Если сравнение положительно, объект - наш клиент, и мы его включаем. Если сравнение отрицательно, либо мы пропускаем объект, либо инициируем исключительную ситуацию.

Благодаря такому подходу можно реализовать полиморфное поведение объектов (операция "On" может быть и у лампочки, и у ядерного реактора). Благодаря такому подходу написанная процедура включения будет успешно работать и в далёком будущем, если на неё подавать объекты, типов которых мы ещё даже не знаем.

В языках со статической типизацией сходное поведение достигается благодаря особым определениям типов, называемым интерфейсами (interface). Это в общем-то те самые образцы (patterns) типов, которые нам могут понадобиться в той или иной ситуации:
Код:
interface IOn { On(); }
class Lamp : IOn { ... }
function On(IOn obj) { obj.On(); }
Недостаток тут всё тот же: нам заранее сложно узнать, какие вообще образцы нам могут понадобиться в будущем. Но уже при описания класса мы вынуждены задать полный и исчерпывающий перечень образцов, которым этот класс соответствует. При этом попытка задать аргумент obj, тип которого (по иерархии обобщения типов и наследования свойств) не сводится к IOn, вызовет ошибку ещё на стадии компиляции.

В языках с динамической типизацией в подавляющем большинстве случаев сравнение с образцом делается автоматически - в рамках интерпретации кода:
Код:
function On(obj) { try { obj.On(); } catch {} }
Т.е. раз написано "obj.On()", значит интерпретатор и ищет в объекте связанную операцию "On". Если находит - выполняет. Если не находит - возникает исключительная ситуация. В некоторых языках для объекта можно задать другой объект - прототип. В таком случае всё не найденное в самом объекте начинается искаться в прототипе, затем в прототипе прототипа и т.д. Таким способом можно реализовывать наследование свойств объектов. Начиная со Smalltalk и далее во всех языках, заимствовавших из него идеи, подобная исключительная ситуация приводит к запросу пользователя: считать ли положение дел сбоем, или же пользователь желает прямо здесь немедленно добавить операцию "On", чтобы продолжить исполнение. Такой диалоговый режим, в котором не различаются фазы разработки и выполнения кода, а код может меняться прямо по ходу работы - это своеобразие Smalltalk-подобных систем, которое теперь начинает представлять определённый интерес с точки зрения распределённых облачных вычислительных систем. В первую очередь тем, что перекликается с темой организации транзакций. Но об этом в другой теме.

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

Предоставляемая динамической типизацией гибкость и простота кода выглядит предпочтительнее. Есть у меня гипотеза, что полезность статической типизации описывается кривой, имеющей максимум лишь в проектах определённого масштаба. Этот масштаб явно превосходит уровень программ на BASIC для Spectrum, но этот же масштаб явно меньше облачных распределённых систем. Коротко говоря, если у нас в итоге получается единый исполняемый файл (или ядро системы), там мы можем использовать статическую типизацию. Если же система представляет собой облако из динамически возникающих и исчезающих, меняющихся сетевых сервисов, при помощи статической типизации мы добьёмся не очень выдающихся результатов, зато обязательно получим массу проблем совместимости версий и обратной совместимости, вороха поддерживаемых старых deprecated и obsolete интерфейсов. Т.е. статическая типизация не может быть центральным элементом идеального языка.

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

Из предыдущей темы, обсуждавшей сложность и многообразие атомов, на основании вышеизложенного можно сделать такой вывод. В идеальном языке должно быть:
- небольшое количество базовых встроенных типов (тех же стандартных числовых типов), являющихся атомами;
- механизм пользовательского описания атомных типов, в качестве которого вполне удовлетворительно смотрится концепция "класса" как АТД, и богатая библиотека таких типов атомов;
- пользовательские/библиотечные атомные типы должны быть максимально примитивными (те же разного рода числа, меры, какие-то объекты для низкоуровневых и высокопроизводительных операций);
- всё остальное должно описываться в виде объектов с регулярной структурой, позволяющих программному коду быть максимально гибким.

В общем-то этой точки зрения придерживаются авторы ряда современных языков, например F#.NET, как раз продвигаемого в качестве средства разработки сетевых приложений.

Возможно, стоит ввести некую специальную конструкцию проверки пригодности структуры, заимствовав идею из языка Eiffel, например:
Код:
assert
  obj.On
fail
  ...
success
  obj.On()
end
или что-то в этом духе.

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

В некоторых языках (в том же F#, в Mathcad) есть понятие "вычисления с единицами измерения". В большинстве языков числовое значение с точки зрения машины и исполняемой программы есть просто математическое число. Семантика того, что это число является некой мерой и имеет соответствующую единицу измерения, оказывается на совести программиста и, разумеется, систематически приводит к ошибкам в расчётных приложениях, где забывают перевести граммы в килограммы, а также разным несуразностям вроде сравнения килограммов с километрами. Единица измерения (unit) - это тоже разновидность типа, неотъемлемый атрибут некоего числового значения. Если в языке заложен механизм контроля единиц измерения и корректного проведения расчётов, то любая программируемая формула помимо чисто арифметических операций над числами ещё сопровождается автоматическим преобразованием единиц измерения (например, из граммов в килограммы), контролем за правильностью (например, килограммы на метры можно умножать, но их нельзя складывать), приведением единиц измерения результата (например, сила, приведённая к СИ, даёт "кг * м / c^2" (ньютон), а момент "Н * с" как произведение силы на время даёт "кг * м * c / c^2", что упрощается до "кг * м / с" и эквивалентно произведению массы на скорость).

Мне представляется, что возможность вспомогательной типизации чисел мерами - это полезная для языка возможность.
« Последнее редактирование: 12-03-2014 20:05 от Dimka » Записан

Программировать - значит понимать (К. Нюгард)
Невывернутое лучше, чем вправленное (М. Аврелий)
Многие готовы скорее умереть, чем подумать (Б. Рассел)
Страниц: [1]   Вверх
  Печать  
 

Powered by SMF 1.1.21 | SMF © 2015, Simple Machines