Наконец-таки появилось русскоязычное руководство по языку Julia. Там реализовано полноценное введение в язык для тех, у кого мало опыта в программировании (остальным будет полезно для общего развития), так же имеется введение в машинное обучение и куча заданий для закрепления материала.  Во время поисков наткнулся на курс программирования для экономистов (помимо Джулии там есть и Питон). Опытные могут пробежаться по экспресс курсу или ознакомиться с книгой How to Think Like a Computer Scientist Далее предоставлен перевод материала из блога Christopher Rackauckas 7 Julia Gotchas and How to Handle Them Позвольте мне начать с того, что Julia замечательный язык. Я люблю её, это то, что я считаю самым мощным и интуитивно понятным языком, который я когда-либо использовал. Это, несомненно, мой любимый язык. Тем не менее, есть некоторые 'подводные камни', хитрые мелочи, о которых вам нужно знать. У каждого языка есть они, и одна из первых вещей, которую вы должны сделать, чтобы овладеть языком, это выяснить, что они из себя представляют и как их избежать. Смысл этого поста состоит в том, чтобы помочь вам ускорить этот процесс, разоблачая некоторые из наиболее распространенных ошибок, предлагающих альтернативные методы программирования. Julia — хороший язык для понимания происходящего, потому что в ней нет магии. Разработчики Julia хотели иметь четко определенные правила поведения. Это означает, что все поведение может быть объяснено. Однако это может означать, что вам придется напрягать голову, чтобы понять, почему происходит именно то, а не другое. Вот почему я не просто собираюсь изложить некоторые общие проблемы, но я также собираюсь объяснить, почему они возникают. Вы увидите, что есть некоторые очень похожие шаблоны, и как только вы их осознаете, вы больше не опростоволоситесь ни на одном из них. Из-за этого у Джулии немного более крутая кривая обучения по сравнению с более простыми языками, такими как MATLAB / R / Python. Однако, как только вы это освоите, вы в полной мере сможете использовать лаконичность Джулии при получении производительности C / Fortran. А теперь копнём глубже. Это, безусловно, самая распространенная проблема, о которой сообщают новые пользователи Julia. Кто-то скажет: "Я слышал, Джулия быстра!", откроет REPL, быстро запишет какой-то хорошо знакомый алгоритм и выполнит этот скрипт. После его выполнения смотрят на время и говорят: "Подождите секунду, почему это медленно, как на Питоне?" Поскольку это такая важная и распространенная проблема, давайте потратим немного времени на изучение причин, по которым это происходит, чтобы понять, как этого избежать. Вы должны понимать, что Джулия — это не только компиляция кода, но и специализация типов (то есть компиляция кода, который специфичен для данных типов). Позвольте мне повторить: Джулия не является быстрой, потому что код компилируется с использованием JIT-компилятора, скорее секрет скорости в том, что компилируется код специфичный для типа. Если вам нужна полная история, ознакомьтесь с некоторыми заметками, которые я написал для предстоящего семинара. Типоспецифичность определяется основным принципом дизайна Юлии: множественной диспетчеризацией. Когда вы пишете код: кажется что это только одна функция, но на самом деле здесь создается большое количество методов. На языке Юлии, функция — это абстракция, а то, что на самом деле вызывается, это метод. Если вы вызовете f(2.0,3.0), то Джулия запустит скомпилированный код, который принимает два числа с плавающей запятой и возвращает значение 2a + b. Если вы вызовете f(2,3), то Джулия запустит другой скомпилированный код, который принимает два целых числа и возвращает значение 2a + b. Функция f является абстракцией или сокращением для множества различных методов, имеющих одинаковую форму, и такая схема использования символа f для вызова всех этих различных методов называется множественной диспетчеризацией. И это применяется повсюду: оператор + на самом деле является функцией, которая будет вызывать методы в зависимости от типов, которые он видит. Julia на самом деле получает свою скорость, потому что скомпилированный ею код знает свои типы, и поэтому скомпилированный код, который вызывает f (2.0,3.0), является именно скомпилированным кодом, который вы получите, определив ту же функцию в C / Fortran. Вы можете проверить это с помощью макроса code_native, чтобы увидеть скомпилированную сборку: Это та же самая скомпилированная сборка, которую вы ожидаете от функции в C / Fortran, и она отличается от кода сборки для целых чисел: Это подводит нас к основной мысли: REPL / Global Scope работает медленно, потому что не допускает спецификацию типа. Прежде всего, обратите внимание, что REPL является глобальной областью действия, потому что Джулия разрешает вложенную область видимости для функций. Например, если мы определим то увидим, что этот код работает. Это потому, что Юлия позволяет вам захватить а из внешней функции во внутреннюю функцию. Если вы примените эту идею рекурсивно, то поймете, что самой высокой областью является область, которая является непосредственно REPL (которая является глобальной областью действия модуля Main). Но теперь давайте подумаем о том, как функция будет компилироваться в этой ситуации. Реализуем то же самое, но используя глобальные переменные: и Вопрос: Какие типы должен принимать компилятор для a и b? Обратите внимание, что в этом примере мы изменили типы и по-прежнему вызывали ту же функцию. Она может иметь дело с любыми типами, которые мы к ней добавляем: плавающими, целыми числами, массивами, странными пользовательскими типами и т. д. На языке Julia это означает, что переменные должны быть в штучной упаковке, и типы проверяются при каждом использовании. Как вы думаете, выглядит скомпилированный код? Для динамических языков без специализации типов этот раздутый код со всеми дополнительными инструкциями настолько хорош, насколько только можно, поэтому Джулия замедляется до их скорости. Чтобы понять, почему это так важно, обратите внимание, что каждый фрагмент кода, который вы пишете в Джулии, компилируется. Допустим, вы пишете цикл в вашем скрипте: Компилятор должен будет скомпилировать этот цикл, но поскольку он не может гарантировать, что типы не изменяются, он консервативно намотает портянку на все типы, что приводит к медленному выполнению. Есть несколько способов избежать этой проблемы. Самый простой способ — всегда оборачивать ваши скрипты в функции. Например, предыдущий код примет вид: Это даст вам тот же результат, но, поскольку компилятор может специализироваться на типе a, он предоставит скомпилированный код, который вы хотите. Еще одна вещь, которую вы можете сделать, это определить ваши переменные как константы. Делая это, вы сообщаете компилятору, что переменная не изменится, и, таким образом, он сможет специализировать весь код, который использует ее, на типе, которым она является в настоящее время. Есть небольшая причуда, что Джулия на самом деле позволяет вам изменять значение константы, но не типа. Таким образом, вы можете использовать const чтоб сообщить компилятору, что вы не будете изменять тип. Тем не менее, обратите внимание, что есть некоторые небольшие причуды: это не работает, как ожидалось, потому что компилятор, понимая, что он знает ответ на f () = a (так как a является константой), просто заменил вызов функции на ответ, давая другое поведение, чем если бы a не был постоянной.  Мораль: не пишите свои сценарии непосредственно в REPL, всегда заключайте их в функцию. Итак, я только что высказал мнение о том, насколько важна специализация кода для типов данных. Позвольте мне задать вопрос, что произойдет, когда ваши типы могут измениться? Если вы догадались: "Ну, вы и в этом случае не можете специализировать скомпилированный код", тогда вы правы. Такая проблема известна как нестабильность типа. Они могут отображаться по-разному, но одним из распространенных примеров является то, что вы инициализируете значение простым, но не обязательно тем типом, которым оно должно быть. Например, давайте посмотрим на: Обратите внимание, что 1/2 — это число с плавающей точкой в ​​Julia. Поэтому, если мы начали с x = 1, целое число изменится на число с плавающей запятой, и, таким образом, функция должна скомпилировать внутренний цикл, как если бы он мог быть любого типа. Если бы вместо этого у нас было: тогда вся функция сможет оптимально компилироваться, зная, что x останется числом с плавающей запятой (эта возможность для компилятора определять типы называется выводом типа). Мы можем проверить скомпилированный код, чтобы увидеть разницу: против Такая разница в количестве вычислений, чтоб получить одно и то же значение!   В этот момент вы можете спросить: "Ну, почему бы просто не использовать C, чтобы не приходилось искать эти нестабильности?" Ответ таков:  Вы можете справиться с нестабильностью с помощью функциональных барьеров  Джулия дает вам макрос code_warntype, чтобы показать, где находятся нестабильности типов. Например, если мы используем это в функции g, которую мы создали: Обратите внимание, что в начале мы говорим, что тип x — Any. Он будет использовать любой тип, который не обозначен как strict type, то есть это абстрактный тип, который необходимо помещать в коробку / проверять на каждом шаге. Мы видим, что в конце мы возвращаем x как UNION {FLOAT64, INT64}, что является еще одним нестрогим типом. Это говорит нам о том, что тип х изменился, вызвав трудности. Если мы вместо этого посмотрим на code_warntype для h, мы получим все строгие типы: Это указывает на то, что функция стабильна по типу и будет компилироваться по существу в оптимальный C-код. Таким образом, нестабильность типов не трудно найти. Что сложнее, так это найти правильный дизайн. Зачем разрешать нестабильность типов? Это давний вопрос, который привел к тому, что динамически типизированные языки доминируют на игровом поле сценариев. Идея заключается в том, что во многих случаях вы хотите найти компромисс между производительностью и надежностью.  Например, вы можете прочитать таблицу с веб-страницы, в которой целые смешаны с числами с плавающей запятой. В Julia вы можете написать свою функцию так, чтобы, если бы все они были целыми числами, она хорошо скомпилировалась, а если бы все они были числами с плавающей запятой, она также хорошо скомпилировалась. А если они смешанные? Это все еще будет работать. Это гибкость / удобство, которое мы знаем и любим из такого языка, как Python / R. Но Джулия прямо скажет вам (через code_warntype), когда вы пожертвуете производительностью.   Есть несколько способов справиться с нестабильностями типов. Прежде всего, если вам нравится что-то вроде C / Fortran, где ваши типы объявлены и не могут измениться (что обеспечивает стабильность типов), вы можете сделать это в Julia: Это делает a 64-разрядным целым числом, и если будущий код попытается изменить его, будет выдано сообщение об ошибке (или будет выполнено правильное преобразование. Но поскольку преобразование не будет автоматически округляться, оно, скорее всего, вызовет ошибки). Посыпьте их вокруг своего кода, и вы получите стабильность типов, аля, C / Fortran. Менее сложный способ справиться с этим — с помощью утверждений типа. Здесь вы помещаете тот же синтаксис на другую сторону знака равенства. Например: Оно как бы говорит: "вычислите b / c и убедитесь, что на выходе есть Float64. Если это не так, попробуйте выполнить автоматическое преобразование. Если преобразование не может быть легко выполнено, выведите ошибку". Размещение таких конструкций поможет вам убедиться, что вы знаете, какие типы участвуют. Однако в некоторых случаях нестабильность типов необходима. Например, допустим, вы хотите иметь надежный код, но пользователь дает вам что-то сумасшедшее, типа: что представляет собой массив 4x1 целых чисел и чисел с плавающей запятой. Фактическим типом элемента для массива является Union {Int64, Float64}, который, как мы видели ранее, был нестрогим, что может привести к проблемам. Компилятор знает только, что каждое значение может быть целым числом или числом с плавающей запятой, но не какой элемент какого типа. Это означает, что наивно выполнять арифметику с этим массивом, например: будет медленным, так как операции будут в штучной упаковке. Однако мы можем использовать множественную диспетчеризацию для запуска кодов специализированным образом. Это известно как использование функциональных барьеров. Например: Обратите внимание, что из-за множественной диспетчеризации вызов inner_foo вызывает либо метод, специально скомпилированный для чисел с плавающей запятой, либо метод, специально скомпилированный для целых чисел. Таким образом, вы можете поместить длинное вычисление в inner_foo и при этом сделать его хорошо работающим, не уступая строгой типизации, которую дает вам функциональный барьер.  Таким образом, я надеюсь, что вы видите, что Юлия предлагает хорошее сочетание производительности строгой типизации и удобства динамической типизации. Хороший программист Julia получает в свое распоряжение и то, и другое, чтобы максимизировать производительность и / или производительность при необходимости.  Одной из самых сильных сторон Юлии являются ее возможности метапрограммирования. Это позволяет вам легко писать программу, генерирующую код, эффективно сокращая объем кода, который вы должны писать и поддерживать. Макрос — это функция, которая запускается во время компиляции и (обычно) выплевывает код. Например: заменит любой экземпляр defa на код a = 5 (:(a = 5) — это quoted expression. Код Джулии — это выражения, и, следовательно, метапрограммирование — это сборка выражений).  Вы можете использовать это, чтобы построить любую сложную программу на Julia, которую пожелаете, и поместить ее в функцию как тип действительно умного сокращения. Однако иногда вам может потребоваться непосредственно оценить сгенерированный код. Юлия дает вам функцию eval или макрос @eval для этого. В общем, вы должны стараться избегать eval, но есть некоторые коды, где это необходимо, например, моя новая библиотека для передачи данных между различными процессами для параллельного программирования. Тем не менее, обратите внимание, что если вы используете его: тогда это будет оцениваться в глобальном масштабе (REPL). Однако исправление такое же, как и для глобальных нестабильностей / типов. Например: не даст хорошего скомпилированного кода, так как a было по существу объявлено в REPL. Но мы можем, например, ввести локальную переменную и присвоить ей тип: Здесь b — это локальная переменная, и компилятор может сделать вывод, что ее тип не изменится, и, таким образом, мы имеем стабильность типов и живем в стране хорошей производительности. Так что работать с eval не сложно, вы просто должны помнить, что он работает в REPL. В Julia есть много случаев, когда ожидается синтаксическое завершение выражения. По этой причине операторы продолжения строки не нужны: Джулия будет просто читать, пока выражение не закончится. Простое правило, верно? Просто убедитесь, что вы помните, как заканчиваются функции. Например: Похоже, что она будет оценивать до 90, но вместо этого дает 27. Почему? Поскольку a = 2 + 3 + 4 + 5 + 6 + 7 является полным выражением, поэтому оно будет иметь значение a = 27, а затем пропускается глупость +8 + 9 + 10+ 11+ 12+ 13, Чтобы продолжить строку, нам нужно было убедиться, что выражение не завершено: Это даст 90, как мы хотели. Это может сбить вас с толку в первый раз, но потом привыкнете.  Более сложная проблема связана с определениями массивов. Например: с первого взгляда можно подумать, что a и b — это одно и то же, но это не так! Первый даст вам (2,2) матрицу, а второй — (1-мерный) вектор размера 2. Чтобы увидеть, в чем проблема, вот более простая версия: В первом случае есть два числа: 1 и -2. Во втором есть выражение: 1-2. Это из-за специального синтаксиса для определений массива. Обычно очень приятно писать: и получать матрицу 2x4. Тем не менее, за удобство приходится расплачиваться. Однако эту проблему также легко избежать: вместо конкатенации с использованием пробела используйте функцию hcat: Задача решена!   Один из способов добиться хороших результатов работы Джулии — работа с "представлениями". "Массив" на самом деле является представлением о непрерывном пятне памяти, которое используется для хранения значений. "Значение" массива — это указатель на ячейку памяти (и информация о его типе). Это дает интересное (и полезное (иногда)) поведение. Например, если мы запустим следующий код: тогда в конце мы получим, что a — это массив , т. е. изменение b изменяет a. Причина в том, что b = a устанавливает значение b равным значению a. Поскольку значение массива является его указателем на ячейку памяти, то, что фактически получает b, не является новым массивом, скорее оно получает указатель на ту же ячейку памяти (именно поэтому изменение b меняет a). Это очень полезно, так как позволяет сохранять один и тот же массив в разных формах. Например, мы можем иметь матрицу и векторную форму матрицы, используя: Теперь b является вектором, но изменение b все еще меняет a, где b индексируется путем считывания столбцов. Обратите внимание, что все это время массивы не копировались, и поэтому эти операции были слишком дешевыми (то есть, нет причин избегать их в чувствительном к производительности коде). Теперь немного подробностей. Обратите внимание, что синтаксис для нарезки массива создаст копию в правой части. Например: создаст новый массив и присвоит ему идентификатор с (таким образом, изменение c не изменит a). Это может быть необходимым поведением, однако обратите внимание, что копирование массивов является дорогостоящей операцией, которую следует избегать, когда это возможно. Таким образом, вместо этого мы создали бы более сложные представления, используя: И d, и e — это одно и то же, и изменение d или e изменит a, потому что оба не будут копировать массив, а просто создавать новые переменные, которые указывают только на первый столбец а. (Другая функция, которая создает представления, — это reshape, которая позволяет изменять форму массива.) Если этот синтаксис находится слева, то это представление. Например: изменит a, потому что в левой части a[1:2,1] совпадает с view (a, 1:2,1), что указывает на ту же память, что и a. Так как копировать-то? Ну, мы можем использовать функцию копирования: Теперь, поскольку b является копией a, а не представлением, изменение b не изменит a. Если мы уже определили a, есть удобная копия copy! (B, a) на месте, которая по существу будет проходить по циклу и записывать значения a в местоположения a (но для этого необходимо, чтобы b был уже определен и с правильным размером). Но теперь давайте сделаем немного более сложный массив. Например, Vector {Vector}: Каждый элемент а является вектором. Что происходит, когда мы копируем? Обратите внимание, что это также изменит a[1][1] на 10! Почему это случилось? Мы использовали copy для копирования значений a. Но значения a были массивами, поэтому мы скопировали указатели в ячейки памяти на b, поэтому b фактически указывает на те же массивы. Чтобы исправить это, используем deepcopy: Это рекурсивно вызывает копирование таким образом, что мы избегаем этой проблемы. Опять же, правила Джулии очень просты и в них нет магии, но иногда вам нужно быть повнимательней.  В MATLAB / Python / R вам советуют использовать векторизацию. В Julia вы, возможно, слышали, что "девекторизованный код лучше". Векторизованные коды дают временные выделения (т. е. они создают промежуточные массивы, которые не нужны, и, как отмечалось ранее, выделения массивов являются дорогостоящими и замедляют ваш код). По этой причине вы захотите объединить ваши векторизованные операции и записать их на месте (in-place), чтобы избежать распределения. Что я имею в виду на месте? Функция in-place (mutable function) — это функция, которая обновляет значение, а не возвращает значение. Если вы собираетесь постоянно работать с массивом, это позволит вам продолжать использовать один и тот же массив вместо создания новых массивов на каждой итерации. Например, если вы написали: затем каждый раз, когда вызывается inner, он будет создавать новый массив, который будет возвращать 2x. Очевидно, нам не нужно продолжать создавать новые массивы. Таким образом, вместо этого у нас может быть кэш-массив y, который будет содержать вывод примерно так: Копнём глубже. inner!(y, x) ничего не возвращает, но меняет y. Поскольку y является массивом, значение y является указателем на фактический массив, и, поскольку в функции эти значения были изменены, inner! (y, x) будет молча изменять значения у. Функции, которые делают это, называются mutable (ну, допустим "мутаторы"). Они обычно обозначаются знаком ! И обычно меняют первый аргумент (это просто так условились).  Таким образом, при вызове inner!(y, x) массив не выделяется. Точно так же copy!(y, x) — это встроенная функция, которая записывает значения x в y, обновляя последний. Как видите, это означает, что каждая операция изменяет только значения массивов. Создаются только два массива: начальный массив для x и начальный массив для y. Первая функция создавала новый массив каждый раз, когда вызывался x + inner(x), и, таким образом, в первой функции было создано 11 массивов. Поскольку распределение массивов стоит дорого, вторая функция будет работать быстрее.  Хорошо, что мы можем работать быстро, но синтаксис немного раздулся, когда нам пришлось выписывать циклы. Вот тут-то и происходит слияние петель (loop-fusion). Начиная с Julia v0.5 вы можете использовать . символ для векторизации любой функции (также известной как широковещание (broadcast), потому что она на самом деле вызывает функцию широковещания). Хотя круто, что f.(x) — это то же самое, что применять f к каждому значению x, но круче то, что циклы сливаются. Если вы просто применили f к x и создали новый массив, то x = x + f. (x) будет иметь копию. Однако вместо этого мы можем обозначить все как векторизованные операции: .= Будет делать поэлементное равенство, так что это, по сути, станет кодом Таким образом, другой способ написать нашу функцию был бы: Поэтому мы все еще получаем краткий векторизованный синтаксис MATLAB / R / Python, но эта версия не создает временных массивов и, следовательно, будет быстрее. Вот как вы можете использовать синтаксис языка сценариев, но при этом получать скорость, подобную C / Fortran. Еще раз повторим: у Юлии нет магии компилятора, только простые правила. Изучите правила хорошо, и все это будет второй натурой. Я надеюсь, что это поможет вам при знакомстве с Джулией. В первый раз, когда вы сталкиваетесь с ошибкой, сложно понять откуда счастья привалило. Но как только вы разберетесь, то уже будете готовы.  Как только вы по-настоящему поймете эти правила, ваш код будет скомпилирован до C / Fortran, в то время как он написан на кратком высокоуровневом языке сценариев. Соедините это с фьюжн-трансляцией и метапрограммированием, и вы получите безумную производительность для объема кода, который вы пишете!  Вот вам вопрос: какие выкидоны Джулии я пропустил? Оставьте комментарий, объясняющий нежданчик и как с этим справиться. Кроме того, ради интереса, какие ваши любимые ошибки из других языков? [Моим должен быть тот факт, что в Javascript внутри функции var x = 3 делает x локальным, а x = 3 делает x глобальным. Автоматические глобалы внутри функций? Это дало некоторые безумные ошибки, из-за которых я больше не хочу использовать Javascript!]