Примечательные отличия от других языков

Примечательные отличия от MATLAB

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

  • Индексы массивов в Julia указываются в квадратных скобках: A[i,j].

  • Когда массив в Julia присваивается другой переменной, он не копируется. После присваивания A = B изменение элементов B приведет к их изменению в A. Чтобы избежать этого, используйте A = copy(B).

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

  • В Julia массивы не увеличиваются автоматически при использовании оператора присваивания. Тогда как в MATLAB с помощью выражения a(4) = 3.2 можно создать массив a = [0 0 0 3.2], а с помощью выражения a(5) = 7 — увеличить его до a = [0 0 0 3.2 7], в Julia соответствующий оператор a[5] = 7 выдает ошибку, если длина a меньше 5 или если имя a впервые используется в этом операторе. В Julia есть функции push! и append!, которые позволяют увеличивать объекты Vector гораздо эффективнее, чем a(end+1) = val в MATLAB.

  • Мнимая единица sqrt(-1) представлена в Julia константой im, а не i или j, как в MATLAB.

  • В Julia числовые литералы без десятичного разделителя (например, 42) создают целые числа, а не числа с плавающей запятой. В результате некоторые операции могут приводить к ошибке выхода за пределы области, если они ожидают число с плавающей запятой. Например, julia> a = -1; 2^a выдаст такую ошибку, так как результат не целочисленный (подробные сведения см. в разделе FAQ об ошибках выхода за пределы области).

  • Если в Julia нужно вернуть или присвоить несколько значений, используются кортежи, например (a, b) = (1, 2) или a, b = 1, 2. В Julia нет функции nargout, которая часто применяется в MATLAB для выполнения дополнительных действий в зависимости от количества возвращенных значений. В аналогичных целях можно использовать необязательные и именованные аргументы.

  • В Julia есть истинные одномерные массивы. Векторы столбцов имеют размер N, а не Nx1. Например, выражение rand(N) создает одномерный массив.

  • В Julia конструкция [x,y,z] всегда создает массив, содержащий три элемента: x, y и z. — Для конкатенации по первому («вертикальному») измерению вызовите функцию vcat(x,y,z) или используйте в качестве разделителя точку с запятой ([x; y; z]). — Для конкатенации по второму («горизонтальному») измерению вызовите функцию hcat(x,y,z) или используйте в качестве разделителя пробел ([x y z]). — Для создания блочных матриц (с конкатенацией по первым двум измерениям) вызовите функцию hvcat или комбинируйте пробелы и точки с запятой в качестве разделителей ([a b; c d]).

  • В Julia конструкции a:b и a:b:c создают объекты AbstractRange. Для создания полного вектора, как в MATLAB, используйте метод collect(a:b). Однако обычно вызывать collect не нужно. Объект AbstractRange в большинстве случаев ведет себя как обычный массив, однако он эффективнее благодаря «ленивому» вычислению значений. Этот шаблон создания специальных объектов вместо полных массивов применяется часто, в том числе с функциями, такими как range, и итераторами, такими как enumerate и zip. Специальные объекты, как правило, можно использовать как обычные массивы.

  • Функции в Julia возвращают результат вычисления последнего выражения или выражения с ключевым словом return. Перечислять имена возвращаемых переменных в определении функции не нужно (подробные сведения см. в разделе Ключевое слово return).

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

  • В Julia при вызове редукций, таких как sum, prod и max, с одним аргументом, например sum(A), они выполняются применительно к каждому элементу массива, даже если у A более одного измерения.

  • В Julia при вызове функции без аргументов необходимо использовать круглые скобки, например rand().

  • В Julia не рекомендуется ставить точку с запятой в конце выражений. Результаты выражений не выводятся на экран автоматически (кроме как в интерактивной командной строке), поэтому строки кода нет необходимости заканчивать точкой с запятой. Для вывода на экран определенных данных можно использовать функцию println или макрос @printf.

  • В Julia, если A и B — массивы, операции логического сравнения, такие как A == B, не возвращают массив логических значений. Вместо этого используйте A .== B или аналогичные выражения для других логических операторов, таких как < и >.

  • В Julia операторы &, | и (xor) выполняются побитово, так же, как, соответственно, and, or и xor в MATLAB. Их приоритет такой же, как у побитовых операторов в Python (но не такой, как в C). Они могут применяться к скалярным значениям или поэлементно к массивам, а также использоваться для объединения логических массивов. Однако обратите внимание на различие в очередности операций: могут потребоваться круглые скобки (например, для выбора элементов A, равных 1 или 2, используйте выражение (A .== 1) .| (A .== 2)).

  • В Julia элементы коллекции могут передаваться в функцию как аргументы с помощью оператора расширения (splat-оператора) ..., например xs=[1,2]; f(xs...).

  • Функция svd в Julia возвращает сингулярные значения в виде вектора, а не плотной диагональной матрицы.

  • В Julia символ ... не применяется для продолжения строк кода. Вместо этого неполное выражение автоматически продолжается в следующей строке.

  • Как в Julia, так и в MATLAB переменной ans присваивается значение последнего выражения, введенного в рамках интерактивного сеанса. Однако в Julia, в отличие от MATLAB, переменная ans не задается, когда код на Julia выполняется в рамках неинтерактивного сеанса.

  • Структуры (struct) в Julia не поддерживают динамическое добавление полей во время выполнения, в отличие от классов (class) в MATLAB. Вместо этого используйте Dict. Словари в Julia не упорядочиваются.

  • В Julia каждый модуль имеет собственную глобальную область или пространство имен, в то время как в MATLAB глобальная область только одна.

  • В MATLAB идиоматическим способом удаления ненужных значений является использование логического индексирования, как в выражении x(x>3) или в операторе x(x>3) = [] для изменения x на месте. В Julia с этой целью предоставляются функции более высокого порядка filter и filter!, которые позволяют использовать выражения filter(z->z>3, x) и filter!(z->z>3, x) в качестве альтернативы для x[x.>3] и x = x[x.>3]. Функция filter! позволяет реже прибегать к временным массивам.

  • Извлечение (или «разыменование») всех элементов массива ячеек, например vertcat(A{:}) в MATLAB, записывается в Julia с помощью оператора расширения (splat-оператор), например vcat(A...).

  • В Julia функция adjoint производит сопряженное транспонирование; в MATLAB adjoint предоставляет присоединенную матрицу (классическое сопряжение), то есть выполняет транспонирование матрицы алгебраических дополнений.

  • В Julia выражение a^b^c вычисляется как a^(b^c), в то время как в MATLAB — как (a^b)^c.

Примечательные отличия от R

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

  • В Julia в одинарные кавычки заключаются символы, а не строки.

  • В Julia подстроки можно создавать по индексам строки. В R перед созданием подстрок строки необходимо преобразовывать в векторы символов.

  • В Julia, так же как в Python, но не так, как в R, строки можно создавать с помощью тройных кавычек """ ... """. Этот синтаксис удобен для создания строк, содержащих разрывы.

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

  • В Julia остаток от целочисленного деления вычисляется с помощью mod(a, b), а не a %% b. % в Julia — это оператор нахождения остатка.

  • В Julia векторы создаются с помощью квадратных скобок. [1, 2, 3] в Julia — это то же самое, что c(1, 2, 3) в R.

  • В Julia не все структуры данных поддерживают логическое индексирование. Более того, логическое индексирование в Julia поддерживается только с помощью векторов той же длины, что и у индексируемых объектов. Пример:

    • В R c(1, 2, 3, 4)[c(TRUE, FALSE)] эквивалентно c(1, 3).

    • В R c(1, 2, 3, 4)[c(TRUE, FALSE, TRUE, FALSE)] эквивалентно c(1, 3).

    • В Julia [1, 2, 3, 4] вызывает исключение BoundsError.

    • В Julia результатом выражения [1, 2, 3, 4] является [1, 3].

  • Как и во многих других языках, в Julia разрешены не все операции с векторами разной длины, в отличие от R, где достаточно, чтобы у векторов был общий диапазон индексов. Например, выражение c(1, 2, 3, 4) + c(1, 2) допустимо в R, но эквивалентное выражение [1, 2, 3, 4] + [1, 2] в Julia вызовет ошибку.

  • В Julia допускается необязательная запятая в конце, если она не меняет смысл кода. Пользователей R это может сбить с толку при обращении к элементам массивов по индексам. Например, выражение x[1,] в R возвращает первую строку матрицы, однако в Julia запятая игнорируется, поэтому x[1,] == x[1] и возвращает первый элемент. Чтобы получить строку, используйте :, как в выражении x[1,:].

  • Функция map в Julia сначала принимает функцию, а затем ее аргументы, в отличие от lapply(<structure>, function, ...) в R. Точно так же эквивалентом функции R apply(X, MARGIN, FUN, ...) в Julia будет функция mapslices, в которую другая функция передается первым аргументом.

  • Многовариантное применение в R, например mapply(choose, 11:13, 1:3), можно записать в Julia в виде broadcast(binomial, 11:13, 1:3). Кроме того, для функций векторизации binomial.(11:13, 1:3) в Julia предлагается синтаксис через точку.

  • Для обозначения конца условных блоков, таких как if, блоков циклов, таких как while и for, и функций в Julia используется ключевое слово end. Вместо однострочной записи if ( cond ) statement в Julia допустимы операторы вида if cond; statement; end, cond && statement и !cond || statement. Операторы присваивания в двух последних синтаксических конструкциях должны явным образом заключаться в круглые скобки, например cond && (x = value).

  • В Julia <-, <<- и -> не являются операторами присваивания.

  • Оператор -> в Julia создает анонимную функцию.

  • Оператор * в Julia позволяет перемножать матрицы, в отличие от R. Если A и B — матрицы, то A * B в Julia означает умножение матриц. В R для этого требуется выражение A %% B. В R такая запись означает поэлементное (адамарово) произведение. В Julia для поэлементного умножения необходимо написать A . B.

  • В Julia для транспонирования матриц служит функция transpose, а для сопряженного транспонирования — оператор ' или функция adjoint. Таким образом, выражение transpose(A) в Julia равносильно t(A) в R. Кроме того, для нерекурсивного транспонирования в Julia есть функция permutedims.

  • В Julia при написании операторов if или циклов for и while круглые скобки не требуются: вместо for (i in c(1, 2, 3)) достаточно написать for i in [1, 2, 3], а вместо if (i == 1) — if i == 1.

  • В Julia числа 0 и 1 не интерпретируются как логические значения. Написать if (1) в Julia нельзя, так как оператор if принимает только логические значения. Вместо этого можно написать if true, if Bool(1) или if 1==1.

  • В Julia нет функций nrow и ncol. Вместо nrow(M) используйте size(M, 1), а вместо ncol(M) — size(M, 2).

  • В Julia скалярные значения, векторы и матрицы четко различаются. В R 1 и c(1) — это одно и то же. В Julia же это не взаимозаменяемые выражения.

  • Функции diag и diagm в Julia работают не так, как в R.

  • В Julia нельзя присвоить результат вызову функции в левой части операции присваивания, то есть нельзя написать diag(M) = fill(1, n).

  • В Julia не рекомендуется вводить лишние функции в главное пространство имен. Большинство статистических функций Julia находятся в пакетах, предлагаемых группой JuliaStats. Например:

    • Функции, относящиеся к вероятностным распределениям, входят в пакет Distributions.

    • Пакет DataFrames предоставляет инструменты для работы с Data Frame.

    • Обобщенные линейные модели предоставляются пакетом GLM.

  • В Julia имеются кортежи и настоящие хэш-таблицы, но нет списков в стиле R. Для возврата нескольких элементов обычно следует использовать кортеж или именованный кортеж: вместо list(a = 1, b = 2) используйте (1, 2) или (a=1, b=2).

  • Julia поощряет пользователей к написанию собственных типов, которые проще в использовании, чем объекты S3 или S4 в R. Система множественной диспетчеризации в Julia означает, что выражения table(x::TypeA) и table(x::TypeB) работают так же, как table.TypeA(x) и table.TypeB(x) в R.

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

  • В Julia конкатенация векторов и матриц производится с помощью функций hcat, vcat и hvcat, а не c, rbind и cbind, как в R.

  • В Julia диапазон наподобие a:b является не краткой формой записи вектора, как в R, а специальным объектом AbstractRange, применяемым для итерации. Преобразовать диапазон в вектор можно так: collect(a:b).

  • Оператор : имеет разный приоритет в R и Julia. В частности, в Julia арифметические операторы имеют приоритет над оператором :, в то время как в R верно обратное. Например, выражение 1:n-1 в Julia эквивалентно 1:(n-1) в R.

  • Функции max и min в Julia эквивалентны соответственно pmax и pmin в R, но аргументы должны иметь одинаковую размерность. Хотя функции maximum и minimum являются аналогами max и min в R, есть важные различия.

  • Функции sum, prod, maximum и minimum в Julia отличаются от своих аналогов в R. Все они принимают необязательный именованный аргумент dims, означающий измерения, с которыми производится операция. Например, возьмем одинаковые матрицы A = [1 2; 3 4] в Julia и B <- rbind(c(1,2),c(3,4)) в R. Тогда sum(A) дает тот же результат, что и sum(B), но sum(A, dims=1) — это строковый вектор, содержащий суммы по каждому столбцу, а sum(A, dims=2) — столбчатый вектор, содержащий суммы по каждой строке. В отличие от этого, в R в данных целях предоставляются отдельные функции colSums(B) и rowSums(B). Если именованный аргумент dims — это вектор, то он определяет все измерения, по которым производится суммирование, с сохранением измерений суммируемого массива, например sum(A, dims=(1,2)) == hcat(10). Следует заметить, что второй аргумент не проверяется на наличие ошибок.

  • В Julia есть ряд функций, которые могут изменять свои аргументы. Например, есть пара функций sort и sort!.

  • В R для обеспечения производительности требуется векторизация. В Julia ситуация почти обратная: зачастую быстрее выполняется код, в котором применяются невекторизированные циклы.

  • В Julia вычисления производятся немедленно; «ленивые» вычисления в стиле R не поддерживаются. Для большинства пользователей это означает редкое применение выражений без кавычек или имен столбцов.

  • В Julia не поддерживается тип NULL. Ближайший аналог — константа nothing, но она действует как скалярное значение, а не список. Вместо is.null(x) используйте x === nothing.

  • В Julia отсутствующие значения представлены объектом missing, а не NA. Вместо is.na(x) используйте ismissing(x) (или ismissing.(x) для поэлементных операций с векторами). Вместо na.rm=TRUE обычно применяется функция skipmissing (хотя в некоторых особых случаях функции принимают аргумент skipmissing).

  • В Julia нет аналога функции R assign или get.

  • В Julia оператор return не требует круглых скобок.

  • В R идиоматическим способом удаления ненужных значений является использование логического индексирования, как в выражении x[x>3] или в операторе x = x[x>3] для изменения x на месте. В Julia с этой целью предоставляются функции более высокого порядка filter и filter!, которые позволяют использовать выражения filter(z->z>3, x) и filter!(z->z>3, x) в качестве альтернативы для x[x.>3] и x = x[x.>3]. Функция filter! позволяет реже прибегать к временным массивам.

Примечательные отличия от Python

  • Блоки for, if, while и т. д. в Julia заканчиваются ключевым словом end. Уровень отступа не имеет значения, как в Python. В отличие от Python, в Julia нет ключевого слова pass.

  • В Julia строки обозначаются двойными кавычками, например "text" (многострочный текст заключается в три пары двойных кавычек), в то время как в Python они могут обозначаться либо одинарными ('text'), либо двойными кавычками ("text"). В Julia одинарные кавычки используются для символов ('c').

  • Для конкатенации строк в Julia служит оператор *, а не +, как в Python. Аналогичным образом, для повтора строк применяется ^, а не *. Неявная конкатенация строковых литералов, возможная в Python (например, 'ab' 'cd' == 'abcd'), в Julia не поддерживается.

  • Списки Python — гибкие, но медленные — соответствуют в Julia типу Vector{Any} или в более общем случае типу Vector{T}, где T — некоторый неконкретный тип элементов. «Быстрые» массивы, такие как массивы NumPy, элементы которых хранятся на месте (то есть тип dtype — np.float64, [('f1', np.uint64), ('f2', np.int32)] и т. д.), могут быть представлены массивами Array{T}, где T — конкретный, неизменяемый тип элементов. Сюда входят как встроенные типы, например Float64, Int32 и Int64, так и более сложные типы, например Tuple{UInt64,Float64}, а также многие пользовательские типы.

  • В Julia индексирование массивов, строк и т. д. начинается с единицы, а не с нуля.

  • При индексировании срезов в Julia последний элемент включается, в отличие от Python. a[2:3] в Julia равносильно a[1:3] в Python.

  • В отличие от Python, в Julia допустимы массивы AbstractArray с произвольными индексами. Отрицательные индексы, имеющие особый смысл в Python, например a[-1] и a[-2], должны записываться в Julia в виде a[end] и a[end-1].

  • Для индексирования до последнего элемента в Julia требуется ключевое слово end. x[1:] в Python эквивалентно x[2:end] в Julia.

  • В Julia символ : перед объектом создает Symbol или заключает выражение в кавычки, поэтому x[:5] равносильно x[5]. Чтобы получить первые n элементов массива, используйте индексирование диапазонов.

  • В Julia индексирование диапазона имеет формат x[start:step:stop], а в Python — формат x[start:(stop+1):step]. Поэтому x[0:10:2] в Python эквивалентно x[1:2:10] в Julia. Аналогичным образом, выражение x[::-1], означающее в Python перевернутый массив, эквивалентно x[end:-1:1] в Julia.

  • В Julia диапазоны можно создавать отдельно в виде start:step:stop, то есть с помощью того же синтаксиса, что и при индексировании массивов. Кроме того, поддерживается функция range.

  • В Julia при индексировании матрицы с помощью массивов, например X[[1,2], [1,3]], происходит обращение к подматрице, являющейся пересечением первой и второй строк с первым и третьим столбцами. В Python выражение X[[1,2], [1,3]] означает обращение к вектору, содержащему значения ячеек [1,1] и [2,3] матрицы. Выражение X[[1,2], [1,3]] в Julia эквивалентно X[np.ix_([0,1],[0,2])] в Python. Выражение X[[0,1], [0,2]] в Python эквивалентно X[[CartesianIndex(1,1), CartesianIndex(2,3)]] в Julia.

  • В Julia нет синтаксиса продолжения строк: если содержимое строки представляет собой полноценное выражение, то строка считается завершенной; в противном случае она продолжается. Один из способов продолжить выражение — заключить его в круглые скобки.

  • В Julia массивы развертываются по столбцам (как в Фортране), в то время как массивы NumPy по умолчанию развертываются по строкам (как в C). Для оптимальной производительности при переборе массивов порядок циклов в Julia должен быть обратным по сравнению с NumPy (см. соответствующий раздел главы «Советы по производительности»).

  • Операторы с присваиванием в Julia (например, +=, -= и т. д.) выполняются не на месте, как в NumPy. Это означает, что выражение A = [1, 1]; B = A; B += [3, 3] не изменяет значения в A. Вместо этого имя B привязывается к результату правой части B = B + 3, который представляет собой новый массив. Для выполнения операций на месте используйте выражения вида B .+= 3 (см. также раздел, посвященный операторам с точкой), явные циклы или InplaceOps.jl.

  • В Julia значения по умолчанию аргументов функции вычисляются при каждом вызове метода, в то время как в Python это происходит только один раз при определении функции. Например, функция f(x=rand()) = x возвращает новое случайное число каждый раз, когда она вызывается без аргумента. С другой стороны, функция g(x=[1,2]) = push!(x,3) возвращает [1,2,3] каждый раз, когда она вызывается как g().

  • В Julia именованные аргументы должны передаваться с указанием имен, в отличие от Python, где их обычно можно передавать как позиционные. Передача именованного аргумента как позиционного изменяет сигнатуру метода, что приводит к созданию исключения MethodError или вызову неверного метода.

  • В Julia % — это оператор нахождения остатка, в то время как в Python — оператор нахождения остатка от целочисленного деления.

  • В Julia часто используемый тип Int соответствует машинному целочисленному типу (Int32 или Int64), в то время как в Python int — это целое число произвольной длины. Это означает, что в Julia тип Int может переполняться, так что 2^64 == 0. Если вам нужно большее значение, выберите другой подходящий тип, например Int128, BigInt или тип с плавающей запятой, такой как Float64.

  • Мнимая единица sqrt(-1) представлена в Julia константой im, а не j, как в Python.

  • В Julia оператором возведения в степень является ^, а не **, как в Python.

  • Для представления пустого значения в Julia служит константа nothing типа Nothing, а в Python — объект None типа NoneType.

  • В Julia при применении стандартного оператора к матричному типу выполняется матричная операция, в то время как в Python — поэлементная операция. Если и A, и B — матрицы, A * B в Julia производит умножение матриц, а не поэлементное умножение, как в Python. Выражение A * B в Julia равносильно A @ B в Python, а A * B в Python равносильно A .* B в Julia.

  • Оператор сопряжения ' в Julia возвращает сопряжение вектора («ленивое» представление строчного вектора), в то время как оператор транспонирования .T применительно к вектору в Python возвращает исходный вектор (холостая операция).

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

  • В Julia нет классов. Вместо этого есть структуры (изменяемые или неизменяемые), содержащие данные, но не методы.

  • Вызов метода экземпляра класса в Python (x = MyClass(*args); x.f(y)) соответствует вызову функции в Julia, например x = MyType(args...); f(x, y). В целом множественная диспетчеризация является более гибкой и эффективной, чем система классов в Python.

  • У структуры Julia может быть только один абстрактный супертип, в то время как классы Python могут наследоваться от одного или нескольких суперклассов (абстрактных или конкретных).

  • В Julia логическая структура программы (пакеты и модули) не зависит от структуры файлов (для включения дополнительных файлов используется ключевое слово include), в то время как в Python структура кода определяется каталогами (пакетами) и файлами (модулями).

  • Тернарный оператор x > 0 ? 1 : -1 в Julia соответствует в Python условному выражению 1 if x > 0 else -1.

  • В Julia символ @ означает макрос, а в Python — декоратор.

  • Обработка исключений в Julia осуществляется с помощью конструкции try — catch — finally, а не try — except — finally. В отличие от Python, в Julia не рекомендуется использовать обработку исключений в рамках обычного порядка выполнения (по сравнению с Python в Julia обычный порядок выполнения быстрее, но исключения перехватываются медленнее).

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

  • Будьте осторожны с неконстантными глобальными переменными в Julia, особенно в сплошных циклах. Так как в Julia (в отличие от Python) можно писать код, близкий к машинному, использование глобальных переменных может иметь серьезные последствия (см. главу Советы по производительности).

  • В Julia округление и усечение производятся явным образом. Вместо выражения Python int(3.7) следует использовать floor(Int, 3.7) или Int(floor(3.7)), причем они отличаются от round(Int, 3.7). floor(x) и round(x) сами по себе возвращают целочисленное значение того же типа, что и x, вместо Int.

  • В Julia анализ текста выполняется явным образом. Выражение Python float("3.7") должно записываться в Julia в виде parse(Float64, "3.7").

  • В Python большинство значений можно использовать как логические (например, if "a": означает, что последующий блок выполняется, а if "": — что он не выполняется). В Julia требуется явное преобразование в тип Bool (например, if "a" вызовет исключение). Для проверки непустой строки в Julia нужно написать явным образом if !isempty(""). Возможно, это покажется странным, но в Python if "False" и bool("False") дают результат True (потому что "False" — это непустая строка); в Julia parse(Bool, "false") возвращает false.

  • В Julia большинство блоков кода, включая циклы и конструкции try — catch — finally, образуют новую локальную область. Обратите внимание, что включения (списковые, генераторы и т. д.) образуют новую локальную область как в Python, так и в Julia, в то время как блоки if не образуют ее в обоих языках.

Примечательные отличия от C/C++

  • Индексы массивов в Julia указываются в квадратных скобках и могут иметь несколько измерений: A[i,j]. Это не просто синтаксический сахар для ссылки на указатель или адрес, как в C/C++. См. раздел руководства, посвященный созданию массивов.

  • В Julia индексирование массивов, строк и т. д. начинается с единицы, а не с нуля.

  • Когда массив в Julia присваивается другой переменной, он не копируется. После присваивания A = B изменение элементов B приведет к их изменению в A. Операторы с присваиванием, например +=, выполняются не на месте. Они эквивалентны A = A + B, то есть левая часть привязывается к результату выражения в правой части.

  • В Julia массивы развертываются по столбцам (как в Фортране), в то время как в C/C++ они по умолчанию развертываются по строкам. Для оптимальной производительности при переборе массивов порядок циклов в Julia должен быть обратным по сравнению с C/C++ (см. соответствующий раздел главы «Советы по производительности»).

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

  • В Julia, в отличие от C/C++, пробелы имеют значение, поэтому их следует добавлять в программу Julia или удалять из нее с осторожностью.

  • В Julia числовые литералы без десятичного разделителя (например, 42) создают целые числа со знаком типа Int, но если литерал не помещается в машинное слово, он автоматически продвигается до типа большего размера, например Int64 (если Int — это тип Int32), Int128 или типа BigInt произвольного размера. Суффиксов числовых литералов, означающих наличие знака, таких как L, LL, U, UL или ULL, нет. Десятичные литералы всегда имеют знак, а шестнадцатеричные (начинающиеся с 0x, как в C/C++) не имеют, если только они не кодируют более 128 битов — в этом случае они имеют тип BigInt. Кроме того, в отличие от C/C++ и Java, а также десятичных литералов в Julia, тип шестнадцатеричного литерала зависит от его длины, включая начальные символы «0s». Например, 0x0 и 0x00 имеют тип UInt8, 0x000 и 0x0000 — тип UInt16, литералы с 5—​8 шестнадцатеричными цифрами — тип UInt32, с 9—​16 шестнадцатеричными цифрами — тип UInt64, с 17—​32 шестнадцатеричными цифрами — тип UInt128, а с более чем 32 шестнадцатеричными цифрами — тип BigInt. Это следует учитывать при определении шестнадцатеричных масок. Например, ~0xf == 0xf0 сильно отличается от ~0x000f == 0xfff0. Битовые литералы длиной 64 бита (Float64) и 32 бита (Float32) выражаются как 1.0 и 1.0f0 соответственно. Литералы с плавающей запятой округляются (и не продвигаются до типа BigFloat), если их нельзя представить в точности. Литералы с плавающей запятой близки по своему поведению к C/C++. Восьмеричные литералы (с префиксом 0o) и двоичные литералы (с префиксом 0b) также относятся к типам без знака (или к типу BigInt, если длина более 128 бит).

  • В Julia оператор деления / возвращает число с плавающей запятой, если оба операнда целочисленного типа. Для целочисленного деления используйте функцию div или оператор ÷.

  • Индексирование массива (Array) с использованием типов с плавающей запятой обычно вызывает ошибку в Julia. Эквивалентом выражения C a[i / 2] в Julia будет a[i ÷ 2 + 1], где i относится к целочисленному типу.

  • Строковые литералы могут разделяться символами " или ""“. Литералы, разделяемые с помощью `""", могут содержать символы " без экранирования (”\""`). В строковые литералы можно интерполировать значения других переменных или выражений следующим образом: $variablename или $(expression). При этом переменная или выражение вычисляется в контексте функции.

  • Символы // означают число типа Rational, а не однострочный комментарий (который обозначается в Julia символом #).

  • Символы #= обозначают начало многострочного комментария, а =# — его конец.

  • Функции в Julia возвращают результат вычисления последнего выражения или выражения с ключевым словом return. Функции могут возвращать несколько значений в виде кортежа, например (a, b) = myfunction() или a, b = myfunction(). Передавать указатели на значения, как в C/C++ (то есть a = myfunction(&b)), не нужно.

  • В Julia не требуется ставить точку с запятой в конце выражений. Результаты выражений не выводятся на экран автоматически (кроме как в интерактивной командной строке REPL), поэтому строки кода нет необходимости заканчивать точкой с запятой. Для вывода на экран определенных данных можно использовать функцию println или макрос @printf. В REPL для подавления вывода можно использовать символ ;. Обратите внимание, что внутри квадратных скобок [ ] символ ; имеет другое значение. Символ ; может применяться для разделения выражений в одной строке, но во многих случаях это необязательно и просто повышает удобочитаемость кода.

  • В Julia оператор (xor) выполняет побитовую операцию XOR (исключающее ИЛИ), то есть ^ в C/C++. Приоритет побитовых операторов не такой, как в C/C++, поэтому могут потребоваться круглые скобки.

  • В Julia ^ — это оператор возведения в степень (pow), а не побитового исключающего ИЛИ, как в C/C++ (для этой цели в Julia используется или xor)

  • В Julia есть два оператора сдвига вправо: >> и >>>. >> производит арифметический сдвиг, а >>> всегда производит логический сдвиг, в отличие от C/C++, где смысл оператора >> зависит от типа сдвигаемого значения.

  • Оператор -> в Julia создает анонимную функцию, а не служит для обращения к члену по указателю.

  • В Julia при написании операторов if или циклов for и while круглые скобки не требуются: вместо for (int i=1; i <= 3; i++) достаточно написать for i in [1, 2, 3], а вместо if (i == 1) — if i == 1.

  • В Julia числа 0 и 1 не интерпретируются как логические значения. Написать if (1) в Julia нельзя, так как оператор if принимает только логические значения. Вместо этого можно написать if true, if Bool(1) или if 1==1.

  • Для обозначения конца условных блоков, таких как if, блоков циклов, таких как while и for, и функций в Julia используется ключевое слово end. Вместо однострочной записи if ( cond ) statement в Julia допустимы операторы вида if cond; statement; end, cond && statement и !cond || statement. Операторы присваивания в двух последних синтаксических конструкциях должны явным образом заключаться в круглые скобки, например cond && (x = value), из-за приоритета операторов.

  • В Julia нет синтаксиса продолжения строк: если содержимое строки представляет собой полноценное выражение, то строка считается завершенной; в противном случае она продолжается. Один из способов продолжить выражение — заключить его в круглые скобки.

  • Макросы Julia работают с проанализированными выражениями, а не с текстом программы, что позволяет им выполнять сложные преобразования кода Julia. Имена макросов начинаются с символа @. Возможен синтаксис наподобие функций (@mymacro(arg1, arg2, arg3)) и наподобие операторов (@mymacro arg1 arg2 arg3). Эти формы взаимозаменяемые: синтаксис наподобие функций особенно полезен, если макрос включен в другое выражение, и обычно понятнее. Синтаксис наподобие операторов часто применяется для аннотирования блоков, как в распределенной конструкции for: @distributed for i in 1:n; #= body =#; end. Если может быть непонятно, где заканчивается конструкция макроса, используйте синтаксис наподобие функций.

  • В Julia есть тип перечисления, который выражается с помощью макроса @enum(name, value1, value2, ...). Пример: @enum(Fruit, banana=1, apple, pear)

  • По соглашению имена функций, которые изменяют свои аргументы, заканчиваются символом !, например push!.

  • В C++ по умолчанию применяется статическая диспетчеризация. Чтобы использовать динамическую диспетчеризацию, необходимо аннотировать функцию как виртуальную. Напротив, в Julia все методы являются «виртуальными» (это следует понимать в более широком смысле, так как методы диспетчеризуются на основе всех типов аргументов, а не только this, по правилу наиболее специфичного объявления).

Julia ⇔ C/C++: Пространства имен

  • Пространства имен (namespace) в C/C++ примерно соответствуют модулям (module) в Julia.

  • Частных глобальных переменных или полей в Julia нет. Все объекты являются общедоступными через полные пути (или при желании относительные).

  • using MyNamespace::myfun (C++) примерно соответствует import MyModule: myfun (Julia).

  • using namespace MyNamespace (C++) примерно соответствует using MyModule (Julia)

    • В Julia только экспортированные (с помощью оператора export) символы доступны вызывающему модулю.

    • В C++ доступны только элементы, указанные во включенных (общедоступных) файлах заголовков.

  • Обратите внимание: ключевые слова import и using в Julia также служат для загрузки модулей (см. ниже).

  • Обратите внимание: ключевые слова import и using в Julia действуют только на уровне глобальной области (module)

    • В C++ using namespace X действует в произвольных областях (например, в области функции).

Julia ⇔ C/C++: Загрузка модулей

  • Если в Julia вам нужно то, что в C/C++ называется библиотекой, то, скорее всего, это будет пакет.

    • Обратите внимание: библиотеки в C/C++ часто вмещают в себя несколько «программных модулей», в то время как в Julia пакет обычно содержит только один такой модуль.

    • Напоминание: модули (module) в Julia — это глобальные области (необязательно «программные модули»).

  • Вместо скриптов сборки или создания (make) в Julia используются «среды проектов» (иногда называемые просто «проектом» или «средой»).

    • Скрипты сборки нужны только для более сложных приложений (например, требующих компиляции или скачивания исполняемых файлов C/C++).

    • Для разработки приложения или проекта в Julia можно инициализировать его корневой каталог как «среду проекта» и поместить в него относящиеся к приложению код и пакеты. Это обеспечивает хороший контроль над зависимостями проекта и воспроизводимость в будущем.

    • Доступные пакеты добавляются в «среду проекта» с помощью функции Pkg.add() или режима Pkg в REPL. (Однако пакет при этом не загружается.)

    • Список доступных пакетов (прямых зависимостей) для «среды проекта» сохраняется в ее файле Project.toml.

    • Полная информация о зависимостях для «среды проекта» генерируется автоматически и сохраняется в файле Manifest.toml посредством Pkg.resolve().

  • Пакеты («программные модули»), доступные «среде проекта», загружаются с помощью оператора import или using.

    • В C/C++ для получения объявлений объектов и функций и их компоновки в библиотеках при сборке исполняемого файла используется оператор #include <moduleheader>.

    • В Julia при повторном вызове using или import существующий модуль просто вводится в область, но не загружается снова (так же, как при добавлении нестандартных директив #pragma once в C/C++).

  • Доступ к репозиториям пакетов на основе каталогов в Julia можно обеспечить путем добавления путей к репозиториям в массив Base.LOAD_PATH.

    • Перед загрузкой пакетов из репозиториев на основе каталогов с помощью оператора import или using не требуется использовать Pkg.add(). Они изначально доступны в рамках проекта.

    • Репозитории пакетов на основе каталогов — это самое быстрое решение для разработки локальных библиотек «программных модулей».

Julia ⇔ C/C++: Сборка модулей

  • В C/C++ файлы .c и .cpp компилируются и добавляются в библиотеку с помощью скриптов сборки или создания (make).

    • В Julia операторы import [PkgName] и using [PkgName] загружают файл [PkgName].jl из подкаталога [PkgName]/src/ пакета.

    • В свою очередь, [PkgName].jl обычно загружает связанные файлы исходного кода посредством вызовов include "[someotherfile].jl".

  • Оператор include "./path/to/somefile.jl" в Julia очень похож на #include "./path/to/somefile.jl" в C/C++.

    • Однако include "..." в Julia не применяется для включения файлов заголовков (это не требуется).

    • Не используйте include "..." в Julia для загрузки кода из других «программных модулей» (вместо этого используйте import или using).

    • include "path/to/some/module.jl" в Julia создает несколько версий одного и того же кода в разных модулях (отдельные типы и т. д. с одинаковыми именами).

    • include "somefile.jl" обычно применяется для сборки нескольких файлов в одном пакете Julia («программном модуле»). Благодаря этому достаточно легко обеспечить однократное включение (include) файлов (нет путаницы с #ifdef).

Julia ⇔ C/C++: Интерфейс модуля

  • В C++ интерфейсы предоставляются с помощью «общедоступных» файлов .h и .hpp, в то время как в Julia модули (module) экспортируют (export) символы, предназначенные для пользователей.

    • В Julia модули (module) зачастую просто добавляют функциональность путем создания новых «методов» существующих функций (например, Base.push!).

    • Поэтому разработчики на Julia не могут полагаться на файлы заголовков в качестве документации по интерфейсу.

    • Интерфейсы для пакетов Julia обычно описываются в строках docstring, файлах README.md, на статических веб-страницах и т. д.

  • Некоторые разработчики предпочитают не экспортировать (export) все символы, необходимые для использования пакета или модуля.

    • Для доступа к этим компонентам пользователю может потребоваться указать функцию, структуру и т. д. с именем пакета или модуля (например, MyModule.run_this_task(...)).

Julia ⇔ C/C++: Краткая справка

Программная концепция Julia C/C++

неименованная область

begin …​ end

{ …​ }

область функции

function x() …​ end

int x() { …​ }

глобальная область

module MyMod …​ end

namespace MyNS { …​ }

программный модуль

«пакет» в Julia

файлы .h или .hpp
со скомпилированным файлом somelib.a

сборка
программных модулей

SomePkg.jl: …​
import("subfile1.jl")
import("subfile2.jl")
…​

$(AR) *.osomelib.a

импорт
программных модулей

import SomePkg

#include <somelib>
со ссылкой в somelib.a

библиотека модулей

LOAD_PATH[], *репозиторий Git,
**пользовательский реестр пакетов

дополнительные файлы .h или .hpp
со скомпилированным файлом somebiglib.a большего размера

  • Диспетчер пакетов в Julia поддерживает регистрацию нескольких пакетов из одного репозитория Git.

  • Это позволяет пользователям размещать библиотеку связанных пакетов в одном репозитории.

    • Реестры Julia в первую очередь предназначены для управления версиями пакетов и их распространения.

    • С помощью пользовательских реестров пакетов можно создать тип библиотеки модулей.

Примечательные отличия от Common Lisp

  • В Julia массивы по умолчанию индексируются от единицы и поддерживают произвольные смещения индексов.

  • Функции и переменные находятся в одном пространстве имен (Lisp-1).

  • Есть тип Pair, но его назначение отличается от COMMON-LISP:CONS. В большинстве случаев (при разделении, построении кортежей и т. д.) различные итерируемые коллекции могут использоваться как взаимозаменяемые. В случае с небольшими коллекциями разнородных элементов тип Tuple ближе всего к спискам Common Lisp. Вместо ассоциативных списков используйте NamedTuple. Для коллекций, имеющих больший размер или содержащих разнородные элементы, следует использовать типы Array и Dict.

  • Типичный рабочий процесс прототипирования в Julia также предполагает непрерывное манипулирование образом, реализуемое пакетом Revise.jl.

  • Из соображений производительности операции в Julia обычно предполагают устойчивость типов. Тогда как для Common Lisp характерно абстрагирование от базовых машинных операций, операции в Julia более тесно согласуются с ними. Пример: — Целочисленное деление с помощью / всегда возвращает результат с плавающей запятой, даже если он точный.  — Оператор // всегда возвращает рациональный результат.  — Оператор ÷ всегда возвращает (усеченный) целочисленный результат. — Сверхбольшие числа поддерживаются, но преобразование не выполняется автоматически; по умолчанию происходит переполнение целочисленных значений. — Комплексные числа поддерживаются, но для получения комплексного результата требуются комплексные входные значения. — Имеется несколько комплексных и рациональных типов с разными типами компонентов.

  • Модули (пространства имен) могут быть иерархическими. Ключевые слова import и using имеют двойное назначение: они загружают код и делают его доступным в пространстве имен. Импорт с помощью import возможен только по имени модуля (что примерно соответствует ASDF:LOAD-OP). Имена слотов не нужно экспортировать отдельно. Глобальным переменным нельзя присваивать значения извне модуля (обойти это ограничение можно с помощью вызова eval(mod, :(var = val))).

  • Имеются макросы (начинаются с символа @), но они не так тесно интегрированы в язык, как в Common Lisp, поэтому и применяются не так широко. Язык поддерживает гигиену макросов. Из-за различий в поверхностном синтаксисе эквивалента COMMON-LISP:&BODY нет.

  • Все функции являются универсальными и используют множественную диспетчеризацию. Списки аргументов не обязательно должны следовать одному и тому же шаблону, что делает возможной эффективную идиоматику (см. описание ключевого слова do). Необязательные и именованные аргументы обрабатываются по-разному. Неоднозначность методов не разрешается, как в Common Lisp Object System, из-за чего для пересечения требуется определение более конкретного метода.

  • Символы не относятся в какому-либо пакету и сами по себе не содержат значений. M.var вычисляет символ var в модуле M.

  • Функциональный стиль программирования полностью поддерживается, включая замыкания, но для Julia он не характерен. При изменении захваченных переменных могут потребоваться некоторые обходные приемы для обеспечения производительности.