среда, 20 августа 2014 г.

Коротко. Ни "о чём". А точнее о монадах и детерминированных функциях

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

Прислали хорошую ссылку про монады
Может быть кто-то расскажет мне про монады?
Техника MapReduce это пример использования монад?
Ещё вопрос: Ленивые вычисления и кешируемость - как-нибудь соотносятся?

О чём я?

Учитывая вот этот пост:

ToDo. Написать про массивы, списки и итераторы

И вот этот комментарий к нему:

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

(надеюсь, что автор не обидится на то, что я его процитировал).

Так вот. Учитывая всё вышесказанное.

Хочу сказать, что я дошёл до того момента, что стал учитывать детерминированность функций и "кешировать результат их выполнения".

Результаты - не то чтобы "прям впечатляющие", но заметные.

Ну и в заключение вспомню вот это:

Awaitable-значения в Delphi

И вот этот мой комментарий:

"Всеволод! Если Вы думаете, что я «придираюсь» к Роману, то Вы — не правы. Просто спросил.
А насчёт FreeAndNil — Вы неправы. Но я не буду начинать этот бесконечный спор.
А что хочу сказать по сути?
Роман продемонстрировал ОФИГЕННУЮ вещь! Просто и со вкусом. СПАСИБО, Роман!
Я обязательно прикручу её к своему коду.
Я даже знаю куда.
1. Тесты иногда запускают несколько потоков, чтобы например обрабатывать асинхронно контекстное меню.
2. В индексаторе есть сортировка слиянием. Независимые пары коробок — можно сортировать асинхронно.
3. В редакторе есть рендеринг пачки параграфов. Его тоже можно делать асинхронно."

Не совсем "в тему", но на самом деле - "пища для ума".

Кстати о "ленивости вычислений".

В "моих скриптах" я достиг её "случайно", таким вот образом:

BOOLEAN operator OR
  BOOLEAN IN aLeft // - параметр слева (передаётся по значению)
  BOOLEAN ^ IN aRight // - параметр справа (передаётся по ссылке)
 // Собственно реализация оператора:
 if aLeft then
  Result := true
  // - если левый параметр (уже вычисленный) истинен, то истинен и сам оператор OR
 else
  Result := Evaluate aRight
  // - иначе вычисляем значение правого параметра
; // OR

Т.е. если мы напишем:

 if true OR ( A AND B ) then
  Print 'true'
 else
  Assert false 'не должны сюда попасть'

-- то выражение ( A AND B ) - "автоматически" вычисляться не будет.

Аналогично определяется и оператор AND:

BOOLEAN operator AND
  BOOLEAN IN aLeft // - параметр слева (передаётся по значению)
  BOOLEAN ^ IN aRight // - параметр справа (передаётся по ссылке)
 // Собственно реализация оператора:
 if aLeft then
  Result := Evaluate aRight
  // - если левый параметр (уже вычисленный) истинен, то вычисляем правый параметр
 else
  Result := false
  // - иначе оператор AND - неистинен
; // AND

Т.е. если мы напишем:

 if false AND ( A AND B ) then
  Print 'false'
 else
  Assert false 'не должны сюда попасть'

-- то выражение ( A AND B ) - "автоматически" вычисляться не будет.

Аналогично можно написать:

VOID operator ?:=
  BOOLEAN IN aLeft1 // - параметр слева, передаваемый по значению
  ANY ^@ IN aLeft2 // - параметр слева, передаваемый по ссылке. 
                   //   Псевдотип ANY указывает на "любой тип".
                   //   Но этот ANY "слева" должен быть совместим с ANY "справа"
                   //   По крайней мере для оператора ^:=
                   //   который называется присвоить значение по ссылке
  ANY ^ IN aRight // - параметр справа, передаваемый по ссылке
  if aLeft1 then
  // - если значение aLeft1 (уже вычисленное) - истинно
   aLeft2 ^:= Evaluate aRight
   // - вычисляем значение aRight и присваиваем его тому, на что указывает aLeft2
  // - иначе ни выражение aLeft2, ни выражение aRight вычислены не будут
; // ?:=

Т.е. если напишем:

INTEGER VAR X
 true X ?:= 1
 // - переменной X будет присвоено значение 1

Или так:

INTEGER VAR X
 false X ?:= 1
 // - переменной X не будет присвоено ничего

А если так:

INTEGER VAR X
 true X ?:= '1'
 // - получим ошибку компиляции "несовместимость типов" 
 //   или ошибку выполнения "невозможно присвоить строку '1' в INTEGER VAR X
 //   ну "как повезёт". Всё зависит от сложности выражений "справа" и "слева". 
 //   Пока далеко не всё вычисляется во "время компиляции", 
 //   но во "время выполнения" - ловится всё

Посмотрим вот на этот комментарий - "иначе ни выражение aLeft2, ни выражение aRight вычислены не будут".

Ну про "ни выражение aRight" - это - по-моему понятно, но когда "выражение aLeft2" надо вычислять?

Разве оно не всегда равно ссылке на переменную?

А вот - не всегда.

Напишем так:

 OBJECT VAR X
 true 
  X -> Y 
  // - вот это выражение вычисляет адрес поля Y на объекте X
   ?:= 1
 // - полю X -> Y будет присвоено значение 1

А если написать так:

 OBJECT VAR X
 false 
  X -> Y 
  // - вот это выражение вычисляет адрес поля Y на объекте X
   ?:= 1
 // - полю X -> Y не будет присвоено ничего, 
 // более того - и само выражение X -> Y - вычисляться не будет

Если же написать так:

VOID operator ?:=
// VOID кстати означаете, что оператор ?:= не должен ничего возвращать, 
// точнее "оставлять на стеке".
// Мы же всё-таки от FORTH наследуемся, посему - по-любому - работаем со "стеком значений"
  BOOLEAN IN aLeft1 // - параметр слева, передаваемый по значению
  ANY ^@ IN aLeft2 // - параметр слева, передаваемый по ссылке. 
                   //   Псевдотип ANY указывает на "любой тип".
                   //   Но этот ANY "слева" должен быть совместим с ANY "справа"
                   //   По крайней мере для оператора ^:=, 
                   //   который называется присвоить значение по ссылке
  ANY ^ IN aRight // - параметр справа, передаваемый по ссылке
  TYPE aLeft2 ConformsTo TYPE aRight
  if aLeft1 then
  // - если значение aLeft1 (уже вычисленное) - истинно
   aLeft2 ^:= Evaluate aRight
   // - вычисляем значение aRight и присваиваем его тому, на что указывает aLeft2
  // - иначе ни выражение aLeft2, ни выражение aRight вычислены не будут
; // ?:=

-- то за счёт предложения TYPE aLeft2 ConformsTo TYPE aRight - совместимость типов будет вычисляться в момент компиляции.

Почему так сложно?

У меня есть несколько объяснений:

1. Так исторически сложилось.
2. Я не знаю как иначе.
3. Это всё таки "оператор", а не функция или процедура. В чём разница? В том, что оператор может применяться к типам ANY, а процедуры и функции - не могут.

Конечно - ни одно из объяснений - мне лично не кажутся убедительными.

Но уж что есть, то есть.

C++ - я писать не планировал, хотя выходит "некое жалкое подобие", да ещё и с элементами "функциональности".

Вот за "подобие" - видимо вот такая расплата.

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

Ну и ещё:

Можно же написать так:

^@ FUNCTION GetFieldY
// - ^@ FUNCTION означает определение функции, возвращающей ссылку на значение, 
//   которое потом может быть выполнено через Evaluate
  OBJECT ^@ IN aSelf // - параметр слева, передаваемый по ссылке
  Result := Evaluate aSelf -> Y
  // - вычисляем поле Y объекта aSelf и возвращаем ссылку на него
  //   Почему тут Evaluate? Объясню ниже.
 ; // GetFieldY

 OBJECT VAR X
 true 
  X GetFieldY 
  // - вот это выражение вычисляет адрес поля Y на объекте X
   ?:= 1
 // - полю X -> Y будет присвоено значение 1

Так вот.

Про Evaluate:
 ^@ FUNCTION GetFieldY
  OBJECT ^@ IN aSelf // - параметр слева, передаваемый по ссылке
  Result := Evaluate aSelf -> Y
  // - вычисляем поле Y объекта aSelf и возвращаем ссылку на него
  //   Почему тут Evaluate? Объясню ниже.
 ; // GetFieldY

 OBJECT VAR X
 true 
  X GetFieldY GetFieldY
  // - вот это выражение вычисляет адрес поля Y на объекте Y на объекте X
   ?:= 1
 // - полю X -> Y -> Y будет присвоено значение 1, 
 //   если выражение X -> Y -> Y - конечно вычисляемое

Сложно. Да. Не спорю.

Но оно не "для конечных пользователей".

А для разработки инфраструктуры для "конечных пользователей".

И ещё.

Уход от ОПЗ:

Понятно, что мы "наследуемся от FORTH".

Там операция сложения выглядит как?

А так:

 2 3 +

-- складываем 2 и 3.

А умножения?

А так:

 2 3 *

-- умножаем 2 и 3.

Как избавиться от ОПЗ?

А вот так:

override 
 ANY 
 // - почему тут ANY, а не INTEGER или DOUBLE?
 //   Именно потому что inherited + применим к РАЗНЫМ типам
 //   Но как же контроль типов? Обратите внимание на TYPE Result ConformsTo TYPE aLeft
  operator +
  ANY ^@ IN aLeft // - параметр слева, передаваемый по ссылке
  ANY ^ IN aRight // - параметр справа, передаваемый по ссылке
  TYPE Result ConformsTo TYPE aLeft
  // - постулируем, что тип Result совместим с aLeft
  TYPE Result ConformsTo TYPE aRight
  // - постулируем, что тип Result совместим с aRight
  Result := ( 
              Evaluate aLeft 
              // - вычисляем значение параметра слева
              Evaluate aRight 
              // - вычисляем значение параметра справа
              inherited +
              // - вызываем оператор +, который "ещё ОПЗ"
            )
; // +

И вот так:

override ANY operator *
  ANY ^@ IN aLeft // - параметр слева, передаваемый по ссылке
  ANY ^ IN aRight // - параметр справа, передаваемый по ссылке
  TYPE Result ConformsTo TYPE aLeft
  // - постулируем, что тип Result совместим с aLeft
  TYPE Result ConformsTo TYPE aRight
  // - постулируем, что тип Result совместим с aRight
  Result := ( 
              Evaluate aLeft 
              // - вычисляем значение параметра слева
              Evaluate aRight 
              // - вычисляем значение параметра справа
              inherited *
              // - вызываем оператор *, который "ещё ОПЗ"
            )
; // *

Тогда:

INTEGER VAR X
 X := 1 + 2

- компилируется как:

X := 1 + 2

А:

INTEGER VAR X
 X := 1 * 2

- компилируется как:

X := 1 * 2

А как компилируется вот это:

INTEGER VAR X
 X := 1 + 2 * 3

?

А вот так:

X := (1 + 2) * 3

А как сделать, чтобы "компилировалось нормально"?

А вот так:

override ANY 
   PRIORITY 0 
 operator +
  ANY ^@ IN aLeft // - параметр слева, передаваемый по ссылке
  ANY ^ IN aRight // - параметр справа, передаваемый по ссылке
  TYPE Result ConformsTo TYPE aLeft
  // - постулируем, что тип Result совместим с aLeft
  TYPE Result ConformsTo TYPE aRight
  // - постулируем, что тип Result совместим с aRight
  Result := ( 
              Evaluate aLeft 
              // - вычисляем значение параметра слева
              Evaluate aRight 
              // - вычисляем значение параметра справа
              inherited +
              // - вызываем оператор +, который "ещё ОПЗ"
            )
; // +

И:

override ANY 
   PRIORITY 1 
 operator *
  ANY ^@ IN aLeft // - параметр слева, передаваемый по ссылке
  ANY ^ IN aRight // - параметр справа, передаваемый по ссылке
  TYPE Result ConformsTo TYPE aLeft
  // - постулируем, что тип Result совместим с aLeft
  TYPE Result ConformsTo TYPE aRight
  // - постулируем, что тип Result совместим с aRight
  Result := ( 
              Evaluate aLeft 
              // - вычисляем значение параметра слева
              Evaluate aRight 
              // - вычисляем значение параметра справа
              inherited *
              // - вызываем оператор *, который "ещё ОПЗ"
            )
; // *

Обратите внимание на строки:

 PRIORITY 0 
 PRIORITY 1

С их учётом:

А как компилируется вот это:

INTEGER VAR X
 X := 1 + 2 * 3

?

А вот так:

X := 1 + ( 2 * 3 )

Почему "так сложно"? Ну потому что "так сложно". Потому что "исторически сложилось".

Ну в C++ - арность и приоритет операций - "зашиты" в компилятор.

У меня - "не зашито" и не "прибито гвоздями".

Зато - сложно.

Но можно сравнить например с Прологом:
Операторы - тоже функторы
Пролог
Основные особенности языка Пролог

Процитирую:

"Некоторые функторы удобнее записывать, как операторы.
Например, можно записать
+(1, 2)
или

                   +
                  / \
                 1   2
Удобнее записать 1+2 , т.е. в виде оператора. Причем надо понимать, что это не операция сложения, а операторная запись структуры. Такие операторы называются инфиксными.
Аналогично операторная запись
2*a+b*c
может быть представлена в виде структуры:
+( *(2, a), *(b, c))
Это и производит пролог при трансляции операторных выражений. Надо четко понимать, что операторы - это другая форма записи структуры."

Может и не покажется уж слишком сложным.

Комментариев нет:

Отправить комментарий