Работа с незнанием имен параметров функции при ее вызове

13

Вот проблема программирования / языка, о которой я хотел бы услышать ваши мысли.

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

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

Функции вытекают из математики, где f (x) имеет ясное значение, потому что функция имеет гораздо более строгое определение, чем обычно в программировании. Чистые функции в математике могут делать намного меньше, чем в программировании, и они являются гораздо более элегантным инструментом, они обычно принимают только один аргумент (обычно это число) и всегда возвращают одно значение (также обычно число). Если функция принимает несколько аргументов, они почти всегда являются просто дополнительными измерениями области функции. Другими словами, один аргумент не важнее других. Конечно, они явно упорядочены, но кроме этого у них нет семантического упорядочения.

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

func DrawRectangleClipped (rectToDraw, fillColor, clippingRect) {}

Глядя на определение, если функция написана правильно, совершенно ясно, что к чему. При вызове функции в вашей IDE / редакторе может даже возникнуть какая-то магия intellisense / завершения кода, которая скажет вам, каким должен быть следующий аргумент. Но ждать. Если мне это нужно, когда я на самом деле пишу звонок, неужели здесь что-то не хватает? Человек, читающий код, не имеет преимущества IDE, и, если он не перейдет к определению, он не знает, какой из двух прямоугольников, переданных в качестве аргументов, для чего используется.

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

DrawRectangleClipped(deserializedArray[0], deserializedArray[1], deserializedArray[2])

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

Как обычно бывает с программированием, есть много потенциальных решений этой проблемы. Многие уже реализованы на популярных языках. Именованные параметры в C # например. Однако все, что я знаю, имеет существенные недостатки. Наименование каждого параметра при каждом вызове функции не может привести к читабельности кода. Такое ощущение, что мы, возможно, перерастаем возможности, которые дает нам простое текстовое программирование. Мы перешли от текста просто в почти каждой области, но мы все еще кодируем то же самое. Нужно больше информации для отображения в коде? Добавьте больше текста. В любом случае, это становится немного тангенциальным, поэтому я остановлюсь здесь.

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

Фактически, если ваши переменные названы в контексте вызываемой функции, вы вводите меньше информации, чем могли бы с этим вызовом функции, и на некотором уровне, если это приводит к ухудшению кода. Если у вас есть процедура, результатом которой является прямоугольник, который вы сохраняете в rectForClipping, а затем другая процедура, которая обеспечивает rectForDrawing, то фактический вызов DrawRectangleClipped - просто церемония. Линия, которая не означает ничего нового и существует только для того, чтобы компьютер знал, чего именно вы хотите, даже если вы уже объяснили это с помощью имен. Это не очень хорошая вещь.

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

Дарвин
источник
2
Я не совсем понимаю, в чем именно заключается проблема ... здесь, кажется, есть несколько идей, но я не уверен, какая из них является вашей главной идеей.
FrustratedWithFormsDesigner
1
В документации по функции должно быть указано, что делают аргументы. Вы можете возразить , что кто - то читает код не может иметь документацию, но не на самом деле знает , что делает этот код и все , что означает , что они извлечь из чтения это догадка. В любом контексте, когда читатель должен знать, что код верен, ему понадобится документация.
Довал
3
@Darwin В функциональном программировании все функции имеют только 1 аргумент. Если вам нужно передать «несколько аргументов», то параметром обычно является кортеж (если вы хотите, чтобы они были упорядочены) или запись (если вы не хотите, чтобы они были). Кроме того, в любое время легко сформировать специализированные версии функций, чтобы вы могли уменьшить количество необходимых аргументов. Поскольку практически каждый функциональный язык предоставляет синтаксис для кортежей и записей, связывание значений безболезненно, и вы получаете композицию бесплатно (вы можете объединять функции, которые возвращают кортежи, с теми, которые принимают кортежи.)
Doval
1
@Bergi Люди склонны обобщать гораздо больше в чистом FP, поэтому я думаю, что сами функции обычно меньше и более многочисленны. Я мог бы быть далеко, хотя. У меня нет большого опыта работы над реальными проектами с Haskell и бандой.
Дарвин
4
Я думаю, что ответ "Не называйте ваши переменные" deserializedArray ""?
whatsisname

Ответы:

10

Я согласен, что то, как часто используются функции, может быть запутанной частью написания кода, и особенно чтения кода.

Ответ на эту проблему частично зависит от языка. Как вы упомянули, C # имеет именованные параметры. Решение Objective-C этой проблемы включает в себя более описательные имена методов. Например, stringByReplacingOccurrencesOfString:withString:это метод с четкими параметрами.

В Groovy некоторые функции принимают карты, допускающие синтаксис, подобный следующему:

restClient.post(path: 'path/to/somewhere',
            body: requestBody,
            requestContentType: 'application/json')

В общем, вы можете решить эту проблему, ограничив число параметров, передаваемых функции. Я думаю, что 2-3 хороший предел. Если кажется, что функции нужно больше параметров, это заставляет меня переосмыслить дизайн. Но это может быть сложнее ответить в целом. Иногда вы пытаетесь сделать слишком много в функции. Иногда имеет смысл рассмотреть класс для хранения ваших параметров. Кроме того, на практике я часто нахожу, что функции, которые принимают большое количество параметров, обычно имеют многие из них как необязательные.

Даже в таком языке, как Objective-C, имеет смысл ограничить количество параметров. Одна из причин заключается в том, что многие параметры являются необязательными. Для примера, см. RangeOfString: и его варианты в NSString .

Шаблон, который я часто использую в Java, - это использование класса в свободном стиле в качестве параметра. Например:

something.draw(new Box().withHeight(5).withWidth(20))

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

Приведенный выше фрагмент кода Java также помогает в тех случаях, когда упорядочение параметров может быть не столь очевидным. Мы обычно предполагаем с координатами, что X предшествует Y. И я обычно вижу высоту перед шириной как соглашение, но это все еще не очень ясно ( something.draw(5, 20)).

Я также видел некоторые функции, например, drawWithHeightAndWidth(5, 20)но даже они не могут принимать слишком много параметров, иначе вы начнете терять читабельность.

Дэвид V
источник
2
Порядок, если вы продолжите на примере Java, действительно может быть очень сложным. Например, сравните следующие конструкторы из awt: Dimension(int width, int height)и GridLayout(int rows, int cols)(количество строк - это высота, значение GridLayoutимеет высоту в первую очередь и Dimensionширину).
Пьер Арло
1
Подобные несоответствия также подвергались критике с помощью PHP ( eev.ee/blog/2012/04/09/php-a-fractal-of-bad-design ), например: array_filter($input, $callback)против array_map($callback, $input)и strpos($haystack, $needle)противarray_search($needle, $haystack)
Пьер Арло
12

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

Это похоже на попытку написать предложение, используя только прилагательные. Поместите туда больше глаголов (вызовов функций), создайте тему (объект) для вашего предложения, и это будет легче читать:

rect.clip(clipRect).fill(color)

Даже если clipRectи colorимеют ужасные имена (а они не должны), вы все равно можете различить их типы из контекста.

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

(rect, clipRect, color) = deserializeClippedRect()
rect.clip(clipRect).fill(color)

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

Карл Билефельдт
источник
1
Мне нравится идея объединения нескольких вызовов функций для уточнения смысла, но разве это не просто танцы вокруг проблемы? По сути, это «я хочу написать предложение, но язык, который я использую, не позволяет мне, поэтому я могу использовать только самый близкий эквивалент»
Дарвин
@Darwin ИМХО, это не так, как можно улучшить, сделав язык программирования более естественным. Естественные языки очень неоднозначны, и мы можем понять их только в контексте и на самом деле никогда не можем быть уверены. Строковые вызовы функций намного лучше, так как каждый термин (в идеале) имеет документацию и доступные источники, а у нас есть скобки и точки, делающие структуру понятной.
Маартин
3

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

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

Telastyn
источник
Фразы действительно кажутся шагом вперед. Я знаю, что некоторые языки имеют схожие возможности, но это FAR от широкого распространения. Не говоря уже о том, что со всей ненавистью к макросам, исходящей от пуристов C (++), которые никогда не использовали макросы, все сделано правильно, у нас может никогда не быть таких функций в популярных языках.
Дарвин
Добро пожаловать в общую тему предметно-ориентированных языках , то я действительно желаю , чтобы больше людей поняли бы преимущество ... (+1)
Izkata
2

В Javascript (или ECMAScript ), к примеру, многие программисты привыкли к

передача параметров в виде набора свойств именованных объектов в одном анонимном объекте.

И как практика программирования, она попала от программистов к их библиотекам и оттуда к другим программистам, которые полюбили это, используют его и пишут еще несколько библиотек и т. Д.

пример

Вместо звонка

function drawRectangleClipped (rectToDraw, fillColor, clippingRect)

как это:

drawRectangleClipped(deserializedArray[0], deserializedArray[1], deserializedArray[2])

, который является действительным и правильным стилем, вы называете

function drawRectangleClipped (params)

как это:

drawRectangleClipped({
    rectToDraw: deserializedArray[0], 
    fillColor: deserializedArray[1], 
    clippingRect: deserializedArray[2]
})

, который является действительным, правильным и приятным в отношении вашего вопроса.

Конечно, для этого должны быть подходящие условия - в Javascript это гораздо более жизнеспособно, чем, скажем, в C. В javascript это даже породило широко используемую в настоящее время структурную нотацию, которая стала популярной в качестве более легкого аналога XML. Он называется JSON (возможно, вы уже слышали об этом).

Павел
источник
Я не знаю достаточно об этом языке, чтобы проверить синтаксис, но в целом мне нравится этот пост. Кажется довольно элегантно. +1
ЭТО Алекс
Довольно часто это объединяется с обычными аргументами, то есть за 1-3 аргументами следуют params(часто содержащие необязательные аргументы и часто сами необязательные), например, эта функция . Это делает функции со многими аргументами довольно легко понять (в моем примере есть 2 обязательных аргумента и 6 параметров аргументов).
Маартин
0

Затем вы должны использовать target-C, вот определение функции:

- (id)performSelector:(SEL)aSelector withObject:(id)anObject withObject:(id)anotherObject

И здесь это используется:

[someObject performSelector:someSelector withObject:someObject2 withObject:someObject3];

Я думаю, что ruby ​​имеет аналогичные конструкции, и вы можете смоделировать их на других языках с помощью списков ключей-значений.

Для сложных функций в Java мне нравится определять фиктивные переменные в формулировке функций. Для вашего примера слева направо:

Rectangle referenceRectangle = leftRectangle;
Rectangle targetRectangle = rightRectangle;
doSomeWeirdStuffWithRectangles(referenceRectangle, targetRectangle);

Похоже, больше кодирования, но вы можете, например, использовать leftRectangle, а затем рефакторинг кода позже с «Извлечь локальную переменную», если вы думаете, что он не будет понят для будущего сопровождающего кода, который может быть или не быть вами.

awsm
источник
Об этом примере Java я написал в вопросе, почему я считаю, что это не очень хорошее решение. Что ты об этом думаешь?
Дарвин
0

Мой подход заключается в создании временных локальных переменных, а не просто вызывать их LeftRectangeи RightRectangle. Скорее я использую несколько более длинные имена, чтобы передать больше значения. Я часто стараюсь максимально различать имена, например, не называть их обоих something_rectangle, если их роль не очень симметрична.

Пример (C ++):

auto& connector_source = deserializedArray[0]; 
auto& connector_target = deserializedArray[1]; 
auto& bounding_box = deserializedArray[2]; 
DoWeirdThing(connector_source, connector_target, bounding_box)

и я мог бы даже написать однострочную функцию-оболочку или шаблон:

template <typename T1, typename T2, typename T3>
draw_bounded_connector(
    T1& connector_source, T2& connector_target,const T3& bounding_box) 
{
    DoWeirdThing(connector_source, connector_target, bounding_box)
}

(игнорируйте амперсанды, если вы не знаете C ++).

Если функция делает несколько странных вещей без хорошего описания - тогда, вероятно, ее необходимо реорганизовать!

einpoklum
источник