Обратная связь, ресурсы и информация для декларативного языка программирования
Я думал о некоторых концепциях, лежащих в основе нового языка. Сначала это было что-то вроде игрушки, но теперь мне интересно, может ли это что-то значить. Я отправляю этот вопрос в Stack Overflow, чтобы узнать, было ли это сделано раньше, и могу ли я получить какие-либо отзывы, идеи или другую информацию.
Я начал думать об этом в основном после прочтения презентации Джонатана Эдварда о декларативном программировании. Затем я смешал это с некоторыми из моих старых идей и тем, что я видел на современных языках.
Основная идея декларативного программирования - "что" против "как". Тем не менее, я слышал это очень много раз, поэтому кажется, что оно почти всегда похоже на слово "интересно", когда оно на самом деле ничего вам не говорит, что расстраивает.
В версии Джонатана Эдварда он сначала начал с ленивых оценок. Это имеет некоторые интересные последствия, а именно функционально-реактивное программирование (FRP). Вот пример FRP с анимацией (используя составленный мной синтаксис):
x as time * 2 // time is some value representing the current time
y as x + (2 * 500)
new Point(x, y)
Так что здесь значения просто автоматически меняются при изменении входных данных. В одном из моих любимых языков, D, было различие между "чистыми" и "нечистыми" функциями. Чистая функция - это функция, которая не имеет связи с внешним миром и использует только другие чистые функции. Иначе это было бы нечисто. Дело в том, что вы всегда можете доверять чистой функции, возвращающей одно и то же значение для заданных аргументов.
Я полагаю, что аналогичный переходный принцип применяется здесь. Наша примесь time
, Все тронуты time
, будучи x
таким образом y
, и поэтому new Point(x, y)
нечисты. Однако обратите внимание (2 * 500)
чисто. Итак, вы видите, что это говорит компилятору, где находятся его ограничения. Я думаю об этом, как об упрощении математического выражения с помощью переменных:
(x ^ 2) + 3x + 5
(4 ^ 2) + 3x + 5 = 16 + 3x + 5 = 21 + 3x = 3(7 + x)
Рассказав компилятору, что чисто, а что нет, мы можем значительно упростить наши программы. Другим моментом являются нетерпеливые или изменчивые данные. Джонатан Эдвард распознал ввод как изменчивый и нетерпеливый, но вывод как функциональный и ленивый. По сути, с учетом нового ввода программа определила изменение атомарного состояния, и тогда выходной результат будет просто функцией текущего состояния. Если вы хотите понять, почему это может быть важно, посмотрите презентацию. Ввод нечист. Ленивая оценка помогает определить изменение состояния атома. Давайте посмотрим, как программа будет написана процедурно:
void main ()
{
String input = "";
writeln("Hello, world!");
writeln("What's your name? ");
input = readln();
writeln("Hello, %s!", input);
writeln("What's your friends name? ");
input = readln();
writeln("Hello to you too, %s!", input);
}
Здесь bind
Ключевое слово говорит, что следующий код выполняется, если begin
изменения. mutable
Ключевое слово говорит, что ввод не ленив, но нетерпелив. Теперь давайте посмотрим, как это может представлять "изменение состояния атома".
program:
mutable step := 0
bind begin:
writeln("Hello, world!")
writeln("What's your name? ")
++step
bind readln() as input when step = 1:
writeln("Hello, %s!", input)
writeln("What's your friends name? ")
++step
bind readln() as input when step = 2:
writeln("Hello to you too, %s!", input)
Теперь здесь мы видим что-то, что можно сделать более простым и понятным для программиста. Прежде всего это уродливый step
переменная и как мы должны увеличивать и тестировать его каждый раз. Вот пример того, как может выглядеть новая и улучшенная версия:
program:
bind begin:
writeln("Hello, world!")
writeln("What's your name? ")
bind readln() as input:
writeln("Hello, %s!", input)
writeln("What's your friends name? ")
yield // This just means the program jumps to here instead of at the beginning
writeln("Hello to you too, %s!", input)
halt
Так-то лучше. Не идеально, хотя. Но если бы я знал идеальный ответ, я бы не был здесь, верно?
Вот лучший пример, используя игровой движок:
class VideoManager:
bind begin: // Basically a static constructor, will only be called once and at the beginning
// Some video set up stuff
bind end: // Basically a static destructor
// Some video shut down stuff
class Input:
quitEvent as handle // A handle is an empty value, but can be updated so code that's bound to it changes.
keyboardEvent as handle(KeyboardEvent) // This handle does return a value though
mouseEvent as handle(MouseEvent)
// Some other code manages actually updating the handles.
class Sprite:
mutable x := 0
mutable y := 0
bind this.videoManager.updateFrame:
// Draw this sprite
class FieldState:
input as new Input
player as new Sprite
bind input.quitEvent:
halt
bind input.keyboardEvent as e:
if e.type = LEFT:
this.player.x -= 2
else if e.type = RIGHT:
this.player.x += 2
else if e.type = UP:
this.player.y -= 2
else if e.type = DOWN:
this.player.y += 2
Мне нравится, что для этого не требуются обратные вызовы, события или даже циклы или что-то еще, а потоки очевидны. Проще сказать, что происходит, и это не просто Python-подобный синтаксис. Я думаю, что это похоже на то, когда разработчики языка поняли, что было только несколько вещей, для которых люди использовали ярлыки и переходы: условные ветви и циклы. Таким образом, они встроили if-then-else, в то время как и для языков, ярлыки и goto стали устаревшими, и компиляторы, а также люди могли сказать, что происходит. Большая часть того, что мы используем, происходит из этого процесса.
Возвращаясь к темам, приятно то, что потоки более гибкие. Если компилятор свободен делать то, что он хочет, потому что мы приблизились к тому, чтобы сказать, что мы хотим, а не как мы хотим, чтобы это было сделано. Таким образом, компилятор может использовать преимущества многоядерных и распределенных процессоров, но все же компенсирует платформы без хорошей поддержки потоков.
Есть еще одна вещь, которую я хотел бы упомянуть. И это мой взгляд на шаблоны. Это было своего рода концептуальное яйцо, которое начало развиваться, когда я начал программировать (на самом деле около 2 лет назад), а затем начал распадаться. В основном это был принцип абстракции, но он простирался дальше, чем классы и объекты.
Это было связано с тем, как я воспринимал функцию. Например:
int add (int a, int b)
{
return a + b;
}
Хорошо, add
вернул int
а что это было? Это вроде как int
жду, чтобы случиться. Как головоломка без нескольких кусочков. Были ограниченные возможности, и подходили только определенные части, но когда вы закончили, у вас был готовый продукт, который вы могли бы затем использовать в другом месте. Это, как я уже сказал, принцип абстракции. Вот несколько примеров того, что я считаю абстракцией + отсутствующими частями -> конкретными отношениями:
- функция + аргументы -> значение
- абстрактный класс + методы -> класс
- класс + значения экземпляра -> объект
- шаблон + аргументы -> функция или класс
- программа + вход + состояние -> выход
Они все тесно связаны. Кажется, этим можно воспользоваться. Но как? Опять же, вот почему это вопрос. Но ленивая оценка здесь интересна, так как вы можете передать что-то, чего не хватает, другому. Для компилятора это в основном вопрос разыменования имен вплоть до примесей. Как мой пример сверху:
(x ^ 2) + 3x + 5
(4 ^ 2) + 3x + 5 = 16 + 3x + 5 = 21 + 3x = 3(7 + x)
Чем больше кусков вы дадите компилятору, тем больше он сможет завершить его и превратить программу в ее основное ядро. И add
указанная выше функция будет автоматически решена во время компиляции, поскольку она не зависит от внешних ресурсов. Можно решить даже множество классов и объектов, а также огромные порции программ, в зависимости от того, насколько умен компилятор.
Это все на данный момент. Если вы видели примеры того, что уже сделано, я хотел бы увидеть. И если у вас есть какие-либо идеи, инновации, ресурсы или отзывы, я бы это тоже оценил.
1 ответ
Вы определенно хотели бы взглянуть на язык программирования Haskell.
Haskell чрезвычайно декларативен, ленивая оценка встроена, и даже существуют функциональные библиотеки реактивного программирования. Но что особенно важно, Haskell является чисто функциональным, то есть все, в действительности все, чисто.
Таким образом, вопрос в том, как Haskell справляется с необходимыми примесями, возникающими при любом IO.
Ответ очень хорошо подходит для ваших мыслей. Haskell использует математическую конструкцию под названием монады, которая в основном представляет вычисление, производящее некоторое значение вместе с функцией bind
(>>=
как инфиксный оператор), что последовательности таких вычислений.
Итак, давайте рассмотрим пример ввода-вывода: прочитайте строку и выведите свое имя... Даже ввод-вывод чистый, поэтому вы не можете просто что-то запустить. Вместо этого вы создаете больше вычислений ввода-вывода
do
putStr "Enter your name: "
name <- getLine
putStrLn ("Hello " ++ name)
Выглядит довольно императивно, но под капотом, это просто синтаксис для
(putStr "Enter your name: ") >>
(getLine >>= \name ->
putStrLn ("Hello " ++ name))
Теперь вы можете определить это bind
/>>=
для произвольных видов вычислений любым удобным для вас способом. Таким образом, фактически все, о чем вы говорили, может быть реализовано таким образом - даже FRP.
Просто попробуйте поискать монады или Haskell здесь, на Stackru; было много вопросов к этой теме. И в конце концов, это все еще проверено на тип, и таким образом корректность может быть обеспечена компилятором.