Диспетчеризация

Множественная диспетчеризация (multiple dispatch) - один из больших плюсов Julia. Она позволяет, сохраняя динамическую природу языка, определять разное поведение функции, в зависимости от типов ее параметров.

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

getdata(source1)

, а вы для Васи написали функцию checkdata:

checkdata(digit, limit) = digit<=limit

Вася собирает все вместе:

allowed = checkdata(getdata(source1), 10)

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

В другом языке программирования, вы возможно пришли бы одному из двух вариантов:

Вариант 1: можно иметь две разные функции: checkdata_max(), проверяющую ограничение сверху и checkdata_range(), проверяющую две границы.

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

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

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

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

checkdata(digit, minlimit, maxlimit) = minlimit<=digit<=maxlimit

Васин новый код выглядит так:

allowed = checkdata(getdata(source1), 10)
allowed2 = checkdata(getdata(source2), -10, 10)

Вскоре выяснилось, что поставщик данных обновил версию ПО и теперь данные источников иногда могут быть строками, и вы решаете, что проверку они должны пройти, только, если в строке не более N символов в первом случае и от N1 до N2 символов - во втором.

Вы с Васей предвидели такой поворот событий. Именно, поэтому вы назвали функцию проверки checkdata а не checkdigit.
Однако, язык Julia вам и здесь преподнес небольшой, но приятный сюрприз: ни в одной из функций:

checkdata(digit, limit) = digit<=limit
checkdata(digit, minlimit, maxlimit) = minlimit<=digit<=maxlimit

(не смотря на то, что Julia - язык с динамической типизацией) вам не придется заниматься анализом полученного типа данных. Все, что вам нужно сделать - это определить новые методы:

checkdata(s::AbstractString, limit) = length(s)<=limit
checkdata(s::AbstractString, limit1, limi2) = limit1<=length(s)<=limit2

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

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

Множественная диспетчеризация тесно связана с системой типов, и здесь я хочу затронуть эту тему.

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

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

Когда же программа готова и работает, на первый план начинает выходить ее надежность (а потом и производительность). Тут другие динамические языки, которые вырвались вперед во время набрасывания кода "на коленке", мало что могут вам предложить, кроме явной проверки полученных значений ( а это снизит и так средненькую производительность программы). С ростом проекта будет расти и потребность обеспечить четкую структуру для некоторых частей программы, и тогда Julia предоставит вам современную и удобную систему типов.

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

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

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

checkdata(digit::Number, limit::Number) = digit<=limit

, а возможно, вы захотите гарантий типа на выходе функции:

checkdata(digit::Number, limit::Number)::Bool = digit<=limit

Это утверждение не даст вам поздно ночью, не подумав, видоизменить функцию так:

checkdata(digit::Number, limit::Number)::Bool = digit<=limit ? "yes" : "no"

Такая конструкция (хоть и только в рантайме, а жаль) проверит свой выходной тип и попробует преобразовать его в булев. А когда не сможет, сообщит вам о проблеме. Это лучше, чем дожидаться, что кто-то на принимающей стороне проверит полученное значение, не так-ли? В реальном идиоматическом коде, который состоит из набора небольших отлаженных функций, и почти в каждой функции могут встречаться вложенные или рекурсивные вызовы, проверки типов предупреждают вас почти на каждом входе (и выходе).

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

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

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

f(x) = x*x
f(x1,x2) = x1*x2

по виду приятно напоминают Haskel или OCaml. Только здесь f(x) - не есть результат автоматического каррирования f(x1,x2), а совершенно другая функция. Хотя, если хотите парциал - сделайте его самостоятельно, как в любом языке с функциями первого класса:

f(x1) = x2->x1+x2

Определения вида:

f(s::AbstractString) = length(s)
f(i::Int) = i
f(n::Number) = floor(n)

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

В коде встречается намного меньше конструкций if else end, чем, когда программируешь в Perl, Python, Ruby и т.д. А когда встречаются - такой код, будет первым кандидатом на переписывание в стиле Julia (и, как правило, после этого станет, более надежном и быстрым).

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

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

results matching ""

    No results matching ""