Рубрики
Без рубрики

Разница между утверждениями и выражениями

По мере того, как я все больше интересуюсь языками программирования — и языками в целом, — я обнаруживаю, что теория действительно такова… С тегами java, информатика, функционал, грамматика.

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

Фон

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

Как ни странно, я действительно усвоил это различие на собственном горьком опыте, когда готовился преподавать курс основ программного обеспечения. В рамках этого тренинга я должен был выполнить все задания по программированию, чтобы получить обратную связь от инструктора. В какой-то момент инструктор упомянул мне, что им не нравится следующий синтаксис Java:

a[++i]

В этом случае у нас есть массив, к которому мы обращаемся через ++i . Другими словами, мы увеличиваем я затем получаю доступ к a по этому индексу — все в одной строке. Видите какие-нибудь проблемы? Если нет, не волнуйтесь! Это тема сегодняшней статьи.

Терминология

Сразу же я хотел бы провести различие между двумя терминами: выражение и утверждение. Эти термины лягут в основу аргумента в пользу того, почему a[++i] считается плохой практикой.

Выражения

В информатике, когда мы говорим о выражениях, мы имеем в виду все, что может быть вычислено для получения значения. Естественно, мы можем думать о любых данных сами по себе как о выражении, потому что данные всегда вычисляются сами по себе:

4
"Hi!"
x
'w'
true
9.0

Конечно, выражения могут состоять из выражений:

4 + 2
"Hi," + " friend!"
x * y
'w' + 4
true == !false
9.0 / 3

В каждом из этих сценариев мы используем операторы для вложения наших выражений, поэтому мы получаем нечто, что может выглядеть как следующая грамматика языка:

: number 
      | () 
      |  *  
      |  +  

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

Заявления

Напротив, операторы ничего не возвращают. Вместо этого они выполняют действие, которое вводит некоторую форму состояния (он же побочный эффект). Следующий список содержит несколько примеров утверждений:

x = 5
if (y) { ... }
while (true) { ... }
return s

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

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

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

((6 * 7) + (5 + 2 + 1)) > 17

Конечно, любая внешняя область будет зависеть от результата некоторой внутренней области, но оценка (6 * 7) не влияет на 17 . В результате очень легко рассуждать о выражении, даже когда его элементы меняются. Добро пожаловать в основы функционального программирования — но это тема для другого времени!

В чем подвох?

К сожалению, в то время как определения, которые я привел, являются четкими, современные языки программирования не всегда придерживаются одних и тех же принципов. Например, это ++i утверждение или выражение? Вопрос с подвохом: это может быть и то, и другое.

В Java ++i и i++ могут использоваться как отдельные операторы для изменения состояния программы. Например, они часто используются для увеличения переменной в цикле for. Однако, кроме того, они могут использоваться в качестве выражений:

a[++i]
a[i++]
someFunction(i++)

Другими словами, ++i возвращает значение, и это значение отличается от i++ . Как вы, вероятно, можете себе представить, эта двусмысленность между утверждениями и выражениями может проявляться в некоторых неприятных ошибках. Например, как вы думаете, что делает следующая программа?

i = 0
while (i < 5) { 
    print(i) 
    i = i++
}

Не вдаваясь в подробности , этот фрагмент кода может выполнять много разных действий. В Java он фактически будет печатать ноль бесконечно, несмотря на явное увеличение i в 4-й строке. Как выясняется, постфиксный оператор ++ возвращает старое значение i после увеличения его значения на единицу. Другими словами, i увеличивается, а затем сбрасывается на ноль.

Последствия двусмысленности между утверждениями и выражениями огромны, и это также распространяется на функции и процедуры.

Но Подождите, Это Еще не все

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

Функции

Подобно математическим функциям, функции программирования возвращают значение, заданное некоторыми входными данными:

int getLength(String s) { ... }
double computeAreaOfSquare(double length) { ... }
double computePotentialEnergy(double m, double g, double h) { ... } 

Другими словами, возвращаемый тип функции не может быть nothing (т.Е. void). В результате функции похожи на выражения: они возвращают значение без каких-либо побочных эффектов. На самом деле, они часто работают вместо выражений:

(getLength(s1) * 2) > getLength(s2)

По определению, функция тогда была бы выражением.

Процедуры

В отличие от этого, процедуры не возвращают значение. Вместо этого они выполняют какое-то действие:

void scale(Square sq, double sc) { ... }
void insertElementAt(int[] list, int index, int element) { ... }
void mutateString(char[] str) { ... }

В результате процедуры более тесно связаны с утверждениями в том смысле, что они вызывают только побочные эффекты. Естественно, они не могут быть использованы в качестве выражений:

mutateString(s) * 4 // What?

По определению, процедура тогда была бы заявлением.

Размывание границ

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

Рассмотрим Java, которая имеет систему pass-by-value/|. Если мы хотим спроектировать структуру данных, мы часто выполняем такие действия, как добавить , удалить , нажать , pop , enqueue , dequeue и т.д. Эти действия интуитивно понятны, потому что они работают так, как мы ожидаем, что они будут работать. Например, если мы хотим добавить элемент в стек, мы собираемся вызвать push с копией элемента в качестве входных данных.

Теперь представьте, что мы хотим реализовать один из методов удаления (т.е. pop ). Как нам это сделать, не стирая граней между функцией и процедурой? Очевидно, что pop имеет побочный эффект: он удаляет верхний элемент из стека. Однако в идеале мы также хотели бы иметь возможность возвращать это значение. Поскольку Java – это передача по значению , мы не можем передать ссылку на элемент обратно вызывающей стороне через один из наших параметров. Другими словами, мы застряли в создании функции с побочными эффектами.

Как следствие, наш метод pop может использоваться либо как выражение, либо как оператор. При использовании в выражении внезапно становится трудно рассуждать о том, что делает это выражение, потому что части этого выражения могут видеть разные состояния стека. Кроме того, последовательные вызовы одного и того же выражения могут давать разные результаты, поскольку состояние стека меняется при каждом вызове.

Тем не менее, есть один способ обойти эту проблему. Мы могли бы создать пару методов, одну функцию и одну процедуру, чтобы получить верхний элемент из стека ( peek ) и удалить этот элемент ( pop ). Идея здесь заключается в том, что мы сохраняем разделение между чистыми функциями и процедурами. Другими словами, мы можем использовать peek , когда хотим узнать, какое значение находится в верхней части стека, не изменяя стек. Затем мы можем использовать |/pop , чтобы удалить этот верхний элемент.

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

Обсуждение

Для меня изучение различия между утверждениями и выражениями вызвало цепную реакцию вопросов о языковом дизайне. В конце концов, миллионы людей по всему миру кодируют, не заботясь об этих деталях, поэтому мой вопрос таков: действительно ли это имеет значение?

В последнее время я заметил тенденцию к функциональному программированию (FP), и мне интересно, является ли это следствием всего технического долга, который образовался из-за размытых границ между выражениями и операторами. Если нет, то действительно ли эта тенденция к FP просто шумиха? В конце концов, FP не нова. Например, Lisp уже более 60 лет, что для технического сообщества составляет целую вечность. О чем вы думаете?

Пока вы здесь, ознакомьтесь с некоторыми из этих связанных статей:

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

Сообщение Разница между утверждениями и выражениями появилось первым на Программист-отступник .

Оригинал: “https://dev.to/renegadecoder94/the-difference-between-statements-and-expressions-1e47”