Робота з невідомою назвою параметрів функції під час її виклику


13

Ось проблема програмування / мови, про яку я хотів би почути ваші думки.

Ми розробили умови, які більшість програмістів (слід) дотримуватися, які не є частиною синтаксису мов, але служать для того, щоб зробити код читабельнішим. Вони, звичайно, завжди є предметом дискусій, але є принаймні деякі основні концепції, які вважають прийнятними більшість програмістів. Доречне іменування змінних, найменування в цілому, роблячи рядки не надмірно довгими, уникаючи довгих функцій, інкапсуляцій, цих речей.

Однак є проблема, що я ще не можу знайти когось, коментуючи це, і що якраз може бути найбільшою з цих груп. Проблема полягає в тому, що аргументи є анонімними під час виклику функції.

Функції випливають з математики, де f (x) має чітке значення, оскільки функція має набагато більш жорстке визначення, яке вона зазвичай робить у програмуванні. Чисті функції з математики можуть зробити набагато менше, ніж вони можуть бути в програмуванні, і вони є набагато більш елегантним інструментом, вони зазвичай беруть лише один аргумент (який, як правило, число), і вони завжди повертають одне значення (також зазвичай число). Якщо функція бере кілька аргументів, вони майже завжди є лише додатковими розмірами домену функції. Іншими словами, один аргумент не важливіший за інші. Вони чітко впорядковані, звичайно, але крім цього вони не мають семантичного впорядкування.

Однак у програмуванні ми маємо більше функцій, що визначають свободу, і в цьому випадку я можу стверджувати, що це не дуже добре. Загальна ситуація, у вас є така функція, як ця

func DrawRectangleClipped (rectToDraw, fillColor, clippingRect) {}

Дивлячись на визначення, якщо функція написана правильно, її абсолютно зрозуміло, що до чого. Під час виклику функції, у вашому IDE / редакторі може виникнути певна магія інтелісценції / заповнення коду, яка підкаже, яким повинен бути наступний аргумент. Але чекай. Якщо мені це потрібно, коли я насправді пишу дзвінок, чи не чогось тут нам не вистачає? Людина, що читає код, не має переваги IDE, і якщо вони не переходять до визначення, вони не мають уявлення, який з двох прямокутників передається як аргументи, для чого використовується.

Проблема іде навіть далі. Якщо наші аргументи походять з якоїсь локальної змінної, можуть виникнути ситуації, коли ми навіть не знаємо, що таке другий аргумент, оскільки ми бачимо лише ім'я змінної. Візьмемо для прикладу цей рядок коду

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

Це полегшується різними розширеннями на різних мовах, але навіть у строго набраних мовах, і навіть якщо ви чітко називаєте свої змінні, ви навіть не згадуєте тип змінної, коли ви передаєте її у функцію.

Як зазвичай це стосується програмування, існує багато потенційних рішень цієї проблеми. Багато з них вже реалізовані популярними мовами. Наприклад, названі параметри в C #. Однак все, що мені відомо, має суттєві недоліки. Іменування кожного параметра при кожному виклику функції не може призвести до читабельного коду. Це майже відчуває, що, можливо, ми переростаємо можливості, які дає нам звичайне програмування тексту. Ми перемістилися з ПОСЛІДНОГО тексту майже в усіх областях, але все одно все-таки кодуємо те саме. Більше інформації потрібно відобразити в коді? Додайте ще текст. У будь-якому випадку це стає трохи дотичним, тому я зупинюсь тут.

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

Насправді, якщо ваші змінні названі в контексті викликаної функції, ви вводите менше інформації, ніж ви, можливо, могли б викликати цю функцію і на якомусь рівні, якщо це призведе до коду гіршого коду. Якщо у вас є процедура, в результаті якої прямокутник, який ви зберігаєте в rectForClipping, а потім інша процедура, що забезпечує rectForDrawing, то фактичний виклик DrawRectangleClipped - це просто церемонія. Рядок, який не означає нічого нового, і чи просто там, щоб комп'ютер знав, що саме ви хочете, хоча ви вже пояснили це своїм іменем. Це не дуже добре.

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


2
Я розгублений у тому, в чому полягає конкретна проблема ... тут, здається, є кілька ідей, не впевнених, яка з них - ваша основна суть.
FrustratedWithFormsDesigner

1
Документація до функції повинна говорити про те, що роблять аргументи. Ви можете заперечити, що хтось, хто читає код, може не мати документації, але тоді насправді не знаєте, що робить код, і який би сенс він не отримував від його читання, - це здогадана здогадка. У будь-якому контексті, коли читачеві потрібно знати, що код правильний, йому знадобиться документація.
Довал

3
@Darwin У функціональному програмуванні всі функції все ще мають лише 1 аргумент. Якщо вам потрібно передати "кілька аргументів", параметр, як правило, є кортежем (якщо ви хочете, щоб вони були впорядковані) або записом (якщо ви не хочете, щоб вони були). Крім того, тривіально формувати спеціалізовані версії функцій у будь-який час, тому ви можете зменшити кількість необхідних аргументів. Оскільки майже кожна функціональна мова надає синтаксис кортежів і записів, зв’язування значень є безболісним, і ви отримуєте композицію безкоштовно (ви можете зв'язати функції, які повертають кортежі з тими, що приймають кортежі.)
Doval

1
@Bergi Люди, як правило, набагато більше узагальнюють у чистому ПП, тому я думаю, що самі функції зазвичай менші та більш численні. Я міг би бути далеко, хоча. Я не маю великого досвіду роботи над реальними проектами з 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, є сенс обмежити кількість параметрів. Одна з причин - багато параметрів необов’язкові. Для прикладу див. ДіапазонOfString: та його зміни в NSString .

Шаблон, який я часто використовую в Java, полягає в тому, щоб використовувати клас вільного стилю як параметр. Наприклад:

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

При цьому клас використовується як параметр, а з класом, що володіє вільним стилем, дозволяє легко читати код.

Наведений вище фрагмент Java також допомагає там, де впорядкування параметрів може бути не таким очевидним. Ми зазвичай припускаємо з координатами, що X приходить до Y. Я зазвичай бачу висоту перед шириною як умову, але це все ще не дуже зрозуміло ( something.draw(5, 20)).

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


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)
П'єр Arlaud

12

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

Це схоже на те, щоб спробувати написати речення, використовуючи лише прикметники. Покладіть туди більше дієслів (функціональних викликів), створіть тему (об’єкт) для свого речення, і це простіше читати:

rect.clip(clipRect).fill(color)

Навіть якщо clipRectі colorє страшні назви (і не повинні), ви можете розрізняти їх тип з контексту.

Ваш дезаріалізований приклад є проблематичним, оскільки контекст виклику намагається зробити занадто багато одразу: дезаріалізувати та намалювати щось. Потрібно призначити імена, які мають сенс і чітко розділити два обов'язки. Як мінімум, щось подібне:

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

Чимало проблем з читабельністю спричинене намаганням бути занадто стислими, пропускаючи проміжні етапи, необхідні людині для виявлення контексту та семантики.


1
Мені подобається ідея струнних кількох функціональних викликів для уточнення сенсу, але хіба це не просто танці навколо проблеми? Це в основному "Я хочу написати речення, але мова, яку я суджу, не дозволить мені, тому я можу використовувати лише найближчий еквівалент"
Дарвін

@Darwin IMHO це не так, як це можна покращити, зробивши мову програмування більш природною мовою. Природні мови дуже неоднозначні, і ми можемо зрозуміти їх лише в контексті і насправді ніколи не можемо бути впевнені. Структування викликів функцій набагато краще, оскільки кожен термін (в ідеалі) має документацію та доступні джерела, і ми маємо круглі дужки та крапки, що дозволяють зрозуміти структуру.
maaartinus

3

На практиці це вирішується кращим дизайном. Винятково рідко, коли добре написані функції беруть більше 2-х входів, і коли це відбувається, не рідко для цих багатьох входів неможливо об'єднати в якийсь згуртований пучок. Це дозволяє досить легко розбивати функції або агрегувати параметри, тому ви не змушуєте функцію робити занадто багато. Один, у якого є два входи, стає легко назвати і набагато зрозуміліше, який саме вхід є.

У моїй іграшці була концепція фрази для вирішення цього питання, а інші, більш природні мови, орієнтовані на мову, мали інші підходи до цього, але всі вони, як правило, мають і інші недоліки. Крім того, навіть фрази - це трохи більше, ніж приємний синтаксис навколо створення функцій, які мають кращі назви. Завжди буде важко зробити гарне ім’я функції, коли це займе купу входів.


Фрази справді здаються кроком вперед. Я знаю, що деякі мови мають подібні можливості, але це 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 варіантів аргументів).
maaartinus

0

Тоді ви повинні використовувати aim-C, ось визначення функції:

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

І тут він використовується:

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

Я думаю, що у ruby ​​є подібні конструкції, і ви можете імітувати їх іншими мовами за допомогою списків key-value.

Для складних функцій на Java мені подобається визначати фіктивні змінні у формулюванні функцій. Для прикладу зліва-справа:

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

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


Про цей приклад 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 ++).

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.