В продолжение темы:
https://programmingmindstream.blogspot.com/2019/11/blog-post.html?m=1
Чем дальше, тем больше - проникаюсь идеями "функциональщины" типа Haskell.
А именно:
1. Константность "объектов". Слово "объекты" - не зря взял в кавычки.
2. Детерминированность результатов функций. Если f ( x, y ) = ( x + y ), то f ( 1, 2 ) - ВСЕГДА вернёт 3. Что бы ни случилось!
3. Кешируемость результатов. Она вытекает из детерминированности.
4. Распараллеливание вычислений. Оно также вытекает из детерминированности.
5. Оперирование не результатами, а функциями. f ( 1, 2 ) на "самом деле даёт" не 3, а @ f ( 1, 2 ), то есть "ссылку на выражение, связанное с параметрами".
6. Из этого вытекает, что программа становится не "последовательностью" императивных инструкций, а деревом "выражений", констант и ссылок на них. И в процессе выполнения программы это дерево "схлопывается" до конечного результата функции main. Ну это грубо говоря. Ввод/вывод Я не беру - это отдельная тема. "Те самые" монады.
7. А из этого вытекает "ленивость вычислений".
8. Ну и кроме всего прочего - "контейнеры". С map, filter, join, sortedJoin, take, intersect, sort, fold, diff etc. Контейнеры при таком подходе можно рассматривать либо как "строки алфавита" если контейнеры не сортированные, тогда все операции над контейнерами переводят "строки одного алфавита" в строки другого. Либо как "множества", если контейнеры сортированные. Тогда мы именно над множествами, которые нам знакомы из теории множеств.
9. Кортежи. Тут не буду особо комментировать. Просто они важны. Они отличаются от скаляров и множеств. Они скорее - "аналоги векторов". Это собственно основа ADT (https://www.ohaskell.guide/adt.html).
10. Операции над функциями как над данным. Связывание функции с параметрами, ссылки на функции, полное и неполное применение функции. Уменьшение арности функции, оно же "каррирование", то есть фиксация одного из параметров функции.
11. Вообще - трактование всего функционала программы не как "последовательность инструкций" (императив). А как преобразование "вход" -> "выход".
12. И в итоге получается совсем другой взгляд на программы. Получается, что программа это функция, которая оперирует скалярами, векторами, и множествами и преобразует "вход" в "выход".
13. Ну и "почти забыл". Но это немаловажно. Трактовка "контейнеров" как структуры head:tail. Голова-хвост. При этом head это "скаляр" относительно данного контейнера, а tail это такой же контейнер, но на элемент меньше. При этом немаловажно понятие [] - пустого контейнера. Аналог 0 для скаляров.
14. Да. Ещё "моноиды". Просто их упомяну. Потому что "вроде понимаю", но сказать не могу. Это множества с операцией и "единичным" (или "нулевым", зависит от трактовки) элементом. Примеры - ( 0, + ), ( 1, * ).
15. Совместимость "простого" типа и выражения, которое "возвращает данный тип":
INTEGER VAR X = 1 - ну тут понятно.
INTEGET VAR Y = @ ( 1 + 2 ) - тут "тонко". На самом деле в Y положится не 3, а ссылка на выражение, которое вычислится ТОЛЬКО когда из Y станут читать значение.
Это очень ВАЖНЫЙ аспект.
16. Автоматический вывод типа выражения. Тоже - просто упомяну. Тут тоже очень долго рассказывать.
17. ДОСТАТОЧНОСТЬ операции < (СТРОГО МЕНЬШЕ) для множества задач. А НЕ Compare, которая возвращает -1, 0, +1.
Это тоже - очень тонкий момент.
18. Нуль-арные конструкторы как "константы типа". У контейнеров "пустой контейнер" или [] - это собственно и есть один из нуль-арных конструкторов. Значение, которое означает "только само себя" и ничего более. Тут с точки зрения императивного программиста прослеживается "аналогия" с Enum. Но только именно "аналогия", и именно в "кавычках". Из нуль-арного конструктора нельзя получить ничего более как СРАВНИТЬ с "самим собой", тогда получим true, или сравнить с другим значением "того же типа", тогда получим false. Тут "спинным мозгом чую, что поллитра". Шучу. А серьёзно - я чую, что это связанно с "моноидами".
19. Любое выражение типа void - имеет "побочные эффекты". Иначе ему незачем быть. Это либо ввод/вывод, либо какой-то другой "императив", а не "функциональщина". Тут тоже видимо просматривается связь с "монадами". Чую что пол-литра, а доказать - не могу.
20. Про "ленивость" вычислений я уже писал? Тут тоже есть тонкий момент:
INTEGER VAR X = @ ( 1 / 0 )
- не вызовет исключения ZeroDivide, в момент присвоения "значения" переменной X (на самом деле это не переменная, а "алиас" к выражению из дерева разбора).
Исключение будет только если мы попытаемся "читать" X, например напишем что-то вроде:
if (X == 2)
или
X Print
21. Тотальное использование рекурсии. Особенно для обработки "контейнеров". Причём в лучшем случае - это "хвостовая рекурсия", то есть БЕЗ переполнения стека. Когда предыдущее значение заменяется следующим.
22. Всё вышеперечисленное приводит к тому, что функционал программы сводится к "алгебре" над значениями (скалярами, векторами и множествами) и "комбинаторике" над функциями (применение к операндам, возврат ссылки, вычисление значений, сравнение etc). Ввод/вывод или random или GetTickCount - не берём. Но они выводятся за рамки данной схемы.
23. Детерминированность вычислений также приводит нас к тому факту, что можно например сравнивать не значения, а выражения:
f ( x, y ) = ( x + y )
f ( 1, 2 ) == f ( 2, 3 )
достаточно сравнить не ВЫЗОВ функции на реальных значениях, а лишь выражения:
@ f ( 1, 2 ) == @ f ( 2, 3 )
Для скаляра это не очевидно, а вот для "контейнеров" - вполне очевидно:
[ 1 2 3 ] filter IsOdd == [ 1 2 3 ] filter IsOdd
- понятно, что в данном случае для сравнения не надо перебирать контейнер.
24. Эквивалентность значений не всегда тождественна:
Boolean Equals ( a, b ) = ( not ( a < b ) and not ( b < a ) )
Тут тоже "тонкий момент". На подумать.
25. Лямбды. Как я про них забыл.
26. (подобнее о пункте 23) выражения "эквивалентнны" если "эквивалентны" их операнды. Тут тонко:
( 1 + 2 ) == ( 1 + 2 ) - равны значения
@ ( 1 + 2 ) == @ ( 1 + 2 ) - равны значения и выражения
VAR X = 1
VAR Y = 2
( X + Y ) == ( X + Y ) - равны значения
@ ( X + Y ) == @ ( X + Y ) - равны значения и выражения
VAR X = 1
VAR Y = 2
( X++ + Y ) == ( X++ + Y ) - НЕ равны значения
@ ( X++ + Y ) == @ ( X++ + Y ) - НЕ равны значения, НО равны выражения
Или:
VAR X = 1
VAR Y = 2
( X++, X + Y ) == ( X++, X + Y ) - НЕ равны значения
@ ( X++, X + Y ) == @ ( X++, X + Y ) - НЕ равны значения, НО равны выражения.
Выражения/функции (связанные или не связанные с операндами) - это значения несколько "иного порядка", чем скаляры, векторы и множества (проще говоря - константы). Над выражениями несколько иная "алгебра" нежели чем над "константами". Что в общем и "понятно", но далеко не всегда "очевидно".
Но таким образом и "код программы" превращается в "алгебру над константами", только там "константами" выступают "выражения".
То есть "сущности более высокого порядка".
И в итоге эти "(функциональные) константы" в результате работы программы схлопываются к ОДНОЙ "(нефункциональной) константе", которая собственно и есть - "результат работы программы".
Мы естественно не берём GUI или "интерактивный" ввод/вывод. Или random или GetTickCount.
Но с ними - "другая история", которая тоже обобщается на эту концепцию. Через "границы разделов", суть - Монады.
Как-то так.
Причём это всё не "сухая теория" из книжек, а принципы которые я осознал и применяю в своём "повседневном программировании".
Естественно это всё написано с точки зрения программиста на ИМПЕРАТИВНОМ языке. И это ни на что не претендует.
Жаль только, что я опять "изобретаю велосипед" и иду тем путём который до меня прошли многие. Но возможно в этом и состоит "программистское Айкидо". Пока сам не "сделаешь"- не поймёшь - что же другие имели в виду.
Зато на этом подходе я сделал уже как минимум три различных фунционала:
1. Генерация кода по модели.
2. Индексация текстов и поиск.
3. Преобразование форматов текстов.
И всё в итоге сводится к операциям над множествами, скалярями и векторами.
Этот список "размышлений" похож на:
1. http://programmingmindstream.blogspot.com/2014/01/blog-post_27.html?m=1
2. http://18delphi.blogspot.com/2013/11/gui_9423.html?m=1
https://programmingmindstream.blogspot.com/2019/11/blog-post.html?m=1
Чем дальше, тем больше - проникаюсь идеями "функциональщины" типа Haskell.
А именно:
1. Константность "объектов". Слово "объекты" - не зря взял в кавычки.
2. Детерминированность результатов функций. Если f ( x, y ) = ( x + y ), то f ( 1, 2 ) - ВСЕГДА вернёт 3. Что бы ни случилось!
3. Кешируемость результатов. Она вытекает из детерминированности.
4. Распараллеливание вычислений. Оно также вытекает из детерминированности.
5. Оперирование не результатами, а функциями. f ( 1, 2 ) на "самом деле даёт" не 3, а @ f ( 1, 2 ), то есть "ссылку на выражение, связанное с параметрами".
6. Из этого вытекает, что программа становится не "последовательностью" императивных инструкций, а деревом "выражений", констант и ссылок на них. И в процессе выполнения программы это дерево "схлопывается" до конечного результата функции main. Ну это грубо говоря. Ввод/вывод Я не беру - это отдельная тема. "Те самые" монады.
7. А из этого вытекает "ленивость вычислений".
8. Ну и кроме всего прочего - "контейнеры". С map, filter, join, sortedJoin, take, intersect, sort, fold, diff etc. Контейнеры при таком подходе можно рассматривать либо как "строки алфавита" если контейнеры не сортированные, тогда все операции над контейнерами переводят "строки одного алфавита" в строки другого. Либо как "множества", если контейнеры сортированные. Тогда мы именно над множествами, которые нам знакомы из теории множеств.
9. Кортежи. Тут не буду особо комментировать. Просто они важны. Они отличаются от скаляров и множеств. Они скорее - "аналоги векторов". Это собственно основа ADT (https://www.ohaskell.guide/adt.html).
10. Операции над функциями как над данным. Связывание функции с параметрами, ссылки на функции, полное и неполное применение функции. Уменьшение арности функции, оно же "каррирование", то есть фиксация одного из параметров функции.
11. Вообще - трактование всего функционала программы не как "последовательность инструкций" (императив). А как преобразование "вход" -> "выход".
12. И в итоге получается совсем другой взгляд на программы. Получается, что программа это функция, которая оперирует скалярами, векторами, и множествами и преобразует "вход" в "выход".
13. Ну и "почти забыл". Но это немаловажно. Трактовка "контейнеров" как структуры head:tail. Голова-хвост. При этом head это "скаляр" относительно данного контейнера, а tail это такой же контейнер, но на элемент меньше. При этом немаловажно понятие [] - пустого контейнера. Аналог 0 для скаляров.
14. Да. Ещё "моноиды". Просто их упомяну. Потому что "вроде понимаю", но сказать не могу. Это множества с операцией и "единичным" (или "нулевым", зависит от трактовки) элементом. Примеры - ( 0, + ), ( 1, * ).
15. Совместимость "простого" типа и выражения, которое "возвращает данный тип":
INTEGER VAR X = 1 - ну тут понятно.
INTEGET VAR Y = @ ( 1 + 2 ) - тут "тонко". На самом деле в Y положится не 3, а ссылка на выражение, которое вычислится ТОЛЬКО когда из Y станут читать значение.
Это очень ВАЖНЫЙ аспект.
16. Автоматический вывод типа выражения. Тоже - просто упомяну. Тут тоже очень долго рассказывать.
17. ДОСТАТОЧНОСТЬ операции < (СТРОГО МЕНЬШЕ) для множества задач. А НЕ Compare, которая возвращает -1, 0, +1.
Это тоже - очень тонкий момент.
18. Нуль-арные конструкторы как "константы типа". У контейнеров "пустой контейнер" или [] - это собственно и есть один из нуль-арных конструкторов. Значение, которое означает "только само себя" и ничего более. Тут с точки зрения императивного программиста прослеживается "аналогия" с Enum. Но только именно "аналогия", и именно в "кавычках". Из нуль-арного конструктора нельзя получить ничего более как СРАВНИТЬ с "самим собой", тогда получим true, или сравнить с другим значением "того же типа", тогда получим false. Тут "спинным мозгом чую, что поллитра". Шучу. А серьёзно - я чую, что это связанно с "моноидами".
19. Любое выражение типа void - имеет "побочные эффекты". Иначе ему незачем быть. Это либо ввод/вывод, либо какой-то другой "императив", а не "функциональщина". Тут тоже видимо просматривается связь с "монадами". Чую что пол-литра, а доказать - не могу.
20. Про "ленивость" вычислений я уже писал? Тут тоже есть тонкий момент:
INTEGER VAR X = @ ( 1 / 0 )
- не вызовет исключения ZeroDivide, в момент присвоения "значения" переменной X (на самом деле это не переменная, а "алиас" к выражению из дерева разбора).
Исключение будет только если мы попытаемся "читать" X, например напишем что-то вроде:
if (X == 2)
или
X Print
21. Тотальное использование рекурсии. Особенно для обработки "контейнеров". Причём в лучшем случае - это "хвостовая рекурсия", то есть БЕЗ переполнения стека. Когда предыдущее значение заменяется следующим.
22. Всё вышеперечисленное приводит к тому, что функционал программы сводится к "алгебре" над значениями (скалярами, векторами и множествами) и "комбинаторике" над функциями (применение к операндам, возврат ссылки, вычисление значений, сравнение etc). Ввод/вывод или random или GetTickCount - не берём. Но они выводятся за рамки данной схемы.
23. Детерминированность вычислений также приводит нас к тому факту, что можно например сравнивать не значения, а выражения:
f ( x, y ) = ( x + y )
f ( 1, 2 ) == f ( 2, 3 )
достаточно сравнить не ВЫЗОВ функции на реальных значениях, а лишь выражения:
@ f ( 1, 2 ) == @ f ( 2, 3 )
Для скаляра это не очевидно, а вот для "контейнеров" - вполне очевидно:
[ 1 2 3 ] filter IsOdd == [ 1 2 3 ] filter IsOdd
- понятно, что в данном случае для сравнения не надо перебирать контейнер.
24. Эквивалентность значений не всегда тождественна:
Boolean Equals ( a, b ) = ( not ( a < b ) and not ( b < a ) )
Тут тоже "тонкий момент". На подумать.
25. Лямбды. Как я про них забыл.
26. (подобнее о пункте 23) выражения "эквивалентнны" если "эквивалентны" их операнды. Тут тонко:
( 1 + 2 ) == ( 1 + 2 ) - равны значения
@ ( 1 + 2 ) == @ ( 1 + 2 ) - равны значения и выражения
VAR X = 1
VAR Y = 2
( X + Y ) == ( X + Y ) - равны значения
@ ( X + Y ) == @ ( X + Y ) - равны значения и выражения
VAR X = 1
VAR Y = 2
( X++ + Y ) == ( X++ + Y ) - НЕ равны значения
@ ( X++ + Y ) == @ ( X++ + Y ) - НЕ равны значения, НО равны выражения
Или:
VAR X = 1
VAR Y = 2
( X++, X + Y ) == ( X++, X + Y ) - НЕ равны значения
@ ( X++, X + Y ) == @ ( X++, X + Y ) - НЕ равны значения, НО равны выражения.
Выражения/функции (связанные или не связанные с операндами) - это значения несколько "иного порядка", чем скаляры, векторы и множества (проще говоря - константы). Над выражениями несколько иная "алгебра" нежели чем над "константами". Что в общем и "понятно", но далеко не всегда "очевидно".
Но таким образом и "код программы" превращается в "алгебру над константами", только там "константами" выступают "выражения".
То есть "сущности более высокого порядка".
И в итоге эти "(функциональные) константы" в результате работы программы схлопываются к ОДНОЙ "(нефункциональной) константе", которая собственно и есть - "результат работы программы".
Мы естественно не берём GUI или "интерактивный" ввод/вывод. Или random или GetTickCount.
Но с ними - "другая история", которая тоже обобщается на эту концепцию. Через "границы разделов", суть - Монады.
Как-то так.
Причём это всё не "сухая теория" из книжек, а принципы которые я осознал и применяю в своём "повседневном программировании".
Естественно это всё написано с точки зрения программиста на ИМПЕРАТИВНОМ языке. И это ни на что не претендует.
Жаль только, что я опять "изобретаю велосипед" и иду тем путём который до меня прошли многие. Но возможно в этом и состоит "программистское Айкидо". Пока сам не "сделаешь"- не поймёшь - что же другие имели в виду.
Зато на этом подходе я сделал уже как минимум три различных фунционала:
1. Генерация кода по модели.
2. Индексация текстов и поиск.
3. Преобразование форматов текстов.
И всё в итоге сводится к операциям над множествами, скалярями и векторами.
Этот список "размышлений" похож на:
1. http://programmingmindstream.blogspot.com/2014/01/blog-post_27.html?m=1
2. http://18delphi.blogspot.com/2013/11/gui_9423.html?m=1
Вообще говоря - я тут попытался пересказать книжку "О Haskell по-человечески" (https://www.ohaskell.guide/). Только словами "императивного программиста".
Комментариев нет:
Отправить комментарий