Часто Задаваемые Вопросы
Сеансы и REPL
Как удалить объект в памяти?
В Julia нет аналога функции MATLAB's clear; после определения имени в сеансе Julia (технически, в модуле Main), оно всегда присутствует.
Если вас беспокоит использование памяти, вы всегда можете заменить объекты на объекты, потребляющие меньше памяти. Например, если A — это массив размером в гигабайт, который вам больше не нужен, вы можете освободить память с помощью A = nothing. Память будет освобождена при следующем запуске сборщика мусора; вы можете принудительно запустить его с помощью gc(). Кроме того, попытка использовать A, вероятно, приведет к ошибке, потому что большинство методов не определены для типа Nothing.
Как изменить объявление типа в моём сеансе?
Возможно, вы определили тип, а затем поняли, что вам нужно добавить новое поле. Если вы попытаетесь сделать это в REPL, получите ошибку:
ERROR: invalid redefinition of constant MyType
Типы в модуле Main не могут быть переопределены.
Хотя это может быть неудобно при разработке нового кода, есть отличное решение. Модули можно заменить, переопределив их, и поэтому, если вы обернёте весь новый код в модуль, вы сможете переопределять типы и константы. Вы не можете импортировать имена типов в Main и ожидать, что сможете их там переопределить, но вы можете использовать имя модуля для разрешения области видимости. Другими словами, во время разработки вы можете использовать рабочий процесс примерно такой:
include("mynewcode.jl") # this defines a module MyModule
obj1 = MyModule.ObjConstructor(a, b)
obj2 = MyModule.somefunction(obj1)
# Got an error. Change something in "mynewcode.jl"
include("mynewcode.jl") # reload the module
obj1 = MyModule.ObjConstructor(a, b) # old objects are no longer valid, must reconstruct
obj2 = MyModule.somefunction(obj1) # this time it worked!
obj3 = MyModule.someotherfunction(obj2, c)
...
Как проверить, что текущий файл выполняется как основной скрипт?
Когда файл выполняется как основной скрипт с помощью julia file.jl, можно активировать дополнительную функциональность, например, обработку аргументов командной строки. Способ определить, что файл выполняется таким образом, — это проверить, является ли abspath(PROGRAM_FILE) == @__FILE__ true.
Функции
Я передал аргумент x в функцию, изменил его внутри этой функции, но снаружи переменная x осталась неизменной. Почему?
Предположим, вы вызываете функцию так:
julia> x = 10
10
julia> function change_value!(y)
y = 17
end
change_value! (generic function with 1 method)
julia> change_value!(x)
17
julia> x # x is unchanged!
10
В Julia привязка переменной x не может быть изменена путём передачи x в качестве аргумента функции. При вызове change_value!(x) в приведённом выше примере y — это новая переменная, первоначально связанная со значением x, то есть 10; затем y пересвязывается с константой 17, в то время как переменная x внешней области видимости остаётся нетронутой.
Но вот на что следует обратить внимание: предположим, что x привязана к объекту типа Array (или любого другого изменяемого типа). Изнутри функции вы не можете «отвязать» x от этого массива, но можете изменить его содержимое. Например:
julia> x = [1,2,3]
3-element Array{Int64,1}:
1
2
3
julia> function change_array!(A)
A[1] = 5
end
change_array! (generic function with 1 method)
julia> change_array!(x)
5
julia> x
3-element Array{Int64,1}:
5
2
3
Здесь мы создали функцию change_array!, которая присваивает 5 первому элементу переданного массива (связанному с x в месте вызова и связанному с A внутри функции). Обратите внимание, что после вызова функции x по-прежнему связана с тем же массивом, но содержимое этого массива изменилось: переменные A и x были отдельными привязками, ссылающимися на один и тот же изменяемый Array объект.
Можно ли использовать using или import внутри функции?
Нет, у вас не должно быть оператора using или import внутри функции. Если вы хотите импортировать модуль, но использовать его символы только внутри конкретной функции или набора функций, у вас есть два варианта:
-
Используйте
import:import Foo function bar(...) # ... refer to Foo symbols via Foo.baz ... endЭто загружает модуль
Fooи определяет переменнуюFoo, которая ссылается на модуль, но не импортирует какие-либо другие символы из модуля в текущее пространство имён. Вы обращаетесь к символамFooпо их полным именамFoo.barи т. д. -
Оборачивайте функцию в модуль:
module Bar export bar using Foo function bar(...) # ... refer to Foo.baz as simply baz .... end end using BarЭто импортирует все символы из
Foo, но только внутри модуляBar.
Что делает оператор ...?
Два использования оператора ...: всасывание и разбрасывание
Многие новички в Julia считают использование оператора ... запутанным. Часть того, что делает оператор ... запутанным, заключается в том, что он означает две разные вещи в зависимости от контекста.
... объединяет множество аргументов в один аргумент в определениях функций
В контексте определений функций оператор ... используется для объединения многих разных аргументов в один аргумент. Это использование оператора ... для объединения многих разных аргументов в один аргумент называется всасыванием:
julia> function printargs(args...)
println(typeof(args))
for (i, arg) in enumerate(args)
println("Arg #$i = $arg")
end
end
printargs (generic function with 1 method)
julia> printargs(1, 2, 3)
Tuple{Int64,Int64,Int64}
Arg #1 = 1
Arg #2 = 2
Arg #3 = 3
Если бы Julia использовала более свободно символы ASCII, оператор всасывания мог бы быть записан как <-... вместо ....
... разбивает один аргумент на многие разные аргументы в вызовах функций
В отличие от использования оператора ... для обозначения всасывания многих разных аргументов в один аргумент при определении функции, оператор ... также используется для разделения одного аргумента функции на многие разные аргументы при использовании в контексте вызова функции. Это использование оператора ... называется разбрасыванием:
julia> function threeargs(a, b, c)
println("a = $a::$(typeof(a))")
println("b = $b::$(typeof(b))")
println("c = $c::$(typeof(c))")
end
threeargs (generic function with 1 method)
julia> x = [1, 2, 3]
3-element Array{Int64,1}:
1
2
3
julia> threeargs(x...)
a = 1::Int64
b = 2::Int64
c = 3::Int64
Если бы Julia использовала более свободно символы ASCII, оператор разбрасывания мог бы быть записан как ...-> вместо ....
Что является значением возврата присваивания?
Оператор = всегда возвращает правую часть, поэтому:
julia> function threeint()
x::Int = 3.0
x # returns variable x
end
threeint (generic function with 1 method)
julia> function threefloat()
x::Int = 3.0 # returns 3.0
end
threefloat (generic function with 1 method)
julia> threeint()
3
julia> threefloat()
3.0
и аналогично:
julia> function threetup()
x, y = [3, 3]
x, y # returns a tuple
end
threetup (generic function with 1 method)
julia> function threearr()
x, y = [3, 3] # returns an array
end
threearr (generic function with 1 method)
julia> threetup()
(3, 3)
julia> threearr()
2-element Array{Int64,1}:
3
3
Типы, объявления типов и конструкторы
Что означает «устойчивость типа»?
Это означает, что тип выходных данных предсказуем по типам входных данных. В частности, это означает, что тип выходных данных не может изменяться в зависимости от значений входных данных. Следующий код не устойчив к типу:
julia> function unstable(flag::Bool)
if flag
return 1
else
return 1.0
end
end
unstable (generic function with 1 method)
Он возвращает либо Int, либо Float64 в зависимости от значения его аргумента. Поскольку Julia не может предсказать тип возвращаемого значения этой функции во время компиляции, любая вычисления, использующая её, должна быть защищена от возможного возникновения обоих типов, что затрудняет создание быстрого машинного кода.
Почему Julia выдаёт DomainError для некоторых, на первый взгляд, осмысленных операций?
Некоторые операции математически осмысленны, но приводят к ошибкам:
julia> sqrt(-2.0) ERROR: DomainError with -2.0: sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)). Stacktrace: [...]
Это поведение является неудобным следствием требования устойчивости типа. В случае с sqrt, большинство пользователей хотят, чтобы sqrt(2.0) возвращало вещественное число, и им не понравится, если оно выдаст комплексное число 1.4142135623730951 + 0.0im. Можно написать функцию sqrt, чтобы переключиться на комплексное значение только при передаче отрицательного числа (что и делает sqrt в некоторых других языках), но тогда результат не будет устойчив к типу, и функция sqrt будет иметь низкую производительность.
В этих и других случаях вы можете получить желаемый результат, выбрав тип входных данных, который указывает вашу готовность принять тип выходных данных, в котором результат может быть представлен:
julia> sqrt(-2.0+0im) 0.0 + 1.4142135623730951im
Почему Julia использует машинную арифметику для целых чисел?
Julia использует машинную арифметику для целых чисел. Это означает, что диапазон Int значений ограничен и переполняется с обоих концов, так что сложение, вычитание и умножение целых чисел могут переполняться или недополняться, что приводит к некоторым результатам, которые могут быть вначале неудивительными:
julia> typemax(Int) 9223372036854775807 julia> ans+1 -9223372036854775808 julia> -ans -9223372036854775808 julia> 2*ans 0
Это явно отличается от поведения математических целых чисел, и вы можете подумать, что для языка высокого уровня неудобно предоставлять пользователю такую возможность. Однако для числовых задач, где эффективность и прозрачность имеют первостепенное значение, альтернативы хуже.
Одна альтернатива — проверить каждую операцию с целыми числами на переполнение и преобразовать результаты в целые типы большего размера, такие как Int128 или BigInt в случае переполнения. К сожалению, это вводит значительную нагрузку на каждую операцию с целыми числами (например, при инкрементировании счётчика цикла) — это требует вывода кода для проверки переполнения во время выполнения после арифметических инструкций и ветвления для обработки потенциальных переполнений. Хуже того, это сделает каждую вычисление с целыми числами неустойчивой к типу. Как мы упоминали выше, устойчивость к типу имеет решающее значение для эффективного создания эффективного кода. Если вы не можете рассчитывать на то, что результатом операций с целыми числами будут целые числа, то невозможно создать быстрый простой код, как это делают компиляторы C и Fortran.
Вариант этого подхода, который позволяет избежать появления нестабильности типов, заключается в объединении типов Int и BigInt в единый гибридный целочисленный тип, который внутренне меняет представление, когда результат больше не помещается в размер машинного целого числа. Хотя это поверхностно избегает нестабильности типа на уровне кода Julia, это просто перекладывает проблему на код C, реализующий этот гибридный целочисленный тип. Этот подход может быть реализован и может быть даже достаточно быстрым во многих случаях, но имеет несколько недостатков. Одна проблема заключается в том, что внутреннее представление целых чисел и массивов целых чисел больше не соответствует естественному представлению, используемому в C, Fortran и других языках с родными машиными целыми числами. Таким образом, для взаимодействия с этими языками нам в конечном итоге придется ввести родные целочисленные типы. Любое неограниченное представление целых чисел не может иметь фиксированное число бит и, следовательно, не может быть храниться в строке массива с фиксированными слотами — большие значения целых чисел всегда требуют отдельного выделенного в куче хранения. И, конечно же, независимо от того, насколько умной является реализация гибридного целочисленного типа, всегда есть ловушки производительности — ситуации, когда производительность неожиданно ухудшается. Сложное представление, отсутствие взаимодействия с C и Fortran, невозможность представления массивов целых чисел без дополнительного хранения в куче и непредсказуемые характеристики производительности делают даже самые умные реализации гибридных целочисленных типов плохим выбором для высокопроизводительных численных задач.
Альтернативой использованию гибридных целых чисел или преобразованию в BigInts является использование насыщающей арифметики целых чисел, где добавление к максимальному значению целого числа оставляет его неизменным, и аналогично для вычитания из минимального значения целого числа. Именно это делает Matlab™:
>> int64(9223372036854775807) ans = 9223372036854775807 >> int64(9223372036854775807) + 1 ans = 9223372036854775807 >> int64(-9223372036854775808) ans = -9223372036854775808 >> int64(-9223372036854775808) - 1 ans = -9223372036854775808
На первый взгляд это кажется достаточно разумным, поскольку 9223372036854775807 намного ближе к 9223372036854775808, чем -9223372036854775808, и целые числа по-прежнему представлены с фиксированным размером естественным образом, совместимым с C и Fortran. Однако насыщающая арифметика целых чисел является глубоко проблематичной. Первая и наиболее очевидная проблема заключается в том, что это не так, как работает арифметика машинных целых чисел, поэтому для реализации насыщающих операций требуется выдача инструкций после каждой машинной целочисленной операции для проверки переполнения или подполнения и замены результата на typemin(Int) или typemax(Int) в соответствии со случаем. Это само по себе расширяет каждую целочисленную операцию от одной быстрой инструкции до полудюжины инструкций, вероятно, включая ветвления. Ой. Но становится еще хуже — насыщающая арифметика целых чисел не ассоциативна. Рассмотрим этот вычисление в Matlab:
>> n = int64(2)^62 4611686018427387904 >> n + (n - 1) 9223372036854775807 >> (n + n) - 1 9223372036854775806
Это затрудняет написание многих основных алгоритмов целых чисел, так как многие распространенные методы зависят от того, что машинная операция сложения с переполнением является ассоциативной. Рассмотрим нахождение середины между целочисленными значениями lo и hi в Julia с помощью выражения (lo + hi) >>> 1:
julia> n = 2^62 4611686018427387904 julia> (n + 2n) >>> 1 6917529027641081856
Видите? Нет проблем. Это правильная середина между 2^62 и 2^63, несмотря на то, что n + 2n равно -4611686018427387904. Теперь попробуйте в Matlab:
>> (n + 2*n)/2 ans = 4611686018427387904
Упс. Добавление оператора >>> в Matlab не помогло бы, потому что насыщение, которое возникает при добавлении n и 2n, уже разрушило информацию, необходимую для вычисления правильной середины.
Отсутствие ассоциативности не только неудобно для программистов, которые не могут полагаться на нее для таких методов, но также препятствует тому, что компиляторы могут делать для оптимизации арифметики целых чисел. Например, поскольку целые числа Julia используют обычную машинную арифметику целых чисел, LLVM свободно агрессивно оптимизирует простые маленькие функции, такие как f(k) = 5k-1. Машинный код этой функции просто таков:
julia> code_native(f, Tuple{Int})
.text
Filename: none
pushq %rbp
movq %rsp, %rbp
Source line: 1
leaq -1(%rdi,%rdi,4), %rax
popq %rbp
retq
nopl (%rax,%rax)
Фактическое тело функции является одной инструкцией leaq, которая вычисляет целочисленное умножение и сложение одновременно. Это еще более выгодно, когда f включается в другую функцию:
julia> function g(k, n)
for i = 1:n
k = f(k)
end
return k
end
g (generic function with 1 methods)
julia> code_native(g, Tuple{Int,Int})
.text
Filename: none
pushq %rbp
movq %rsp, %rbp
Source line: 2
testq %rsi, %rsi
jle L26
nopl (%rax)
Source line: 3
L16:
leaq -1(%rdi,%rdi,4), %rdi
Source line: 2
decq %rsi
jne L16
Source line: 5
L26:
movq %rdi, %rax
popq %rbp
retq
nop
Поскольку вызов f включается, тело цикла оказывается просто одной инструкцией leaq. Далее рассмотрим, что происходит, если мы сделаем число итераций цикла постоянным:
julia> function g(k)
for i = 1:10
k = f(k)
end
return k
end
g (generic function with 2 methods)
julia> code_native(g,(Int,))
.text
Filename: none
pushq %rbp
movq %rsp, %rbp
Source line: 3
imulq $9765625, %rdi, %rax # imm = 0x9502F9
addq $-2441406, %rax # imm = 0xFFDABF42
Source line: 5
popq %rbp
retq
nopw %cs:(%rax,%rax)
Поскольку компилятор знает, что сложение и умножение целых чисел являются ассоциативными, а умножение распределяется по сложению — ни одно из которых неверно для насыщающей арифметики — он может оптимизировать весь цикл до простого умножения и сложения. Насыщающая арифметика полностью подавляет такой вид оптимизации, поскольку ассоциативность и дистрибутивность могут не выполняться на каждой итерации цикла, вызывая различные результаты в зависимости от того, на какой итерации произошел сбой. Компилятор может развернуть цикл, но не может алгебраически свести несколько операций к меньшим эквивалентным операциям.
Наиболее разумной альтернативой тому, чтобы арифметика целых чисел молча переполнялась, является выполнение проверки арифметики во всех случаях, поднятие ошибок при переполнении операций сложения, вычитания и умножения, производя значения, которые не являются правильно рассчитанными. В этой статье блога Дэн Лу выявляет это и обнаруживает, что вместо тривиальной стоимости, которую этот подход должен иметь теоретически, он оказывается существенной из-за того, что компиляторы (LLVM и GCC) не оптимизируют вокруг добавленных проверок переполнения. Если это улучшится в будущем, мы можем рассмотреть возможность по умолчанию использования проверенной целочисленной арифметики в Julia, но пока мы должны жить с возможностью переполнения.
Какие могут быть причины ошибки UndefVarError во время удаленного выполнения?
Как указывает ошибка, непосредственной причиной ошибки UndefVarError на удаленном узле является то, что связывание с таким именем не существует. Давайте рассмотрим некоторые возможные причины.
julia> module Foo
foo() = remotecall_fetch(x->x, 2, "Hello")
end
julia> Foo.foo()
ERROR: On worker 2:
UndefVarError: Foo not defined
Stacktrace:
[...]
Замыкание x->x содержит ссылку на Foo, и поскольку Foo недоступно на узле 2, возникает ошибка UndefVarError.
Глобальные переменные в модулях, отличных от Main, не сериализуются по значению на удаленный узел. Отправляется только ссылка. Функции, которые создают глобальные связывания (за исключением Main) могут привести к ошибке UndefVarError позже.
julia> @everywhere module Foo
function foo()
global gvar = "Hello"
remotecall_fetch(()->gvar, 2)
end
end
julia> Foo.foo()
ERROR: On worker 2:
UndefVarError: gvar not defined
Stacktrace:
[...]
В приведенном выше примере @everywhere module Foo определил Foo на всех узлах. Однако вызов Foo.foo() создал новое глобальное связывание gvar на локальном узле, но это не было найдено на узле 2, что привело к ошибке UndefVarError.
Обратите внимание, что это не относится к глобальным переменным, созданным в модуле Main. Глобальные переменные в модуле Main сериализуются, а новые связывания, созданные в Main на удаленном узле.
julia> gvar_self = "Node1" "Node1" julia> remotecall_fetch(()->gvar_self, 2) "Node1" julia> remotecall_fetch(varinfo, 2) name size summary ––––––––– –––––––– ––––––– Base Module Core Module Main Module gvar_self 13 bytes String
Это не относится к объявлениям function или struct. Однако анонимные функции, связанные с глобальными переменными, сериализуются, как показано ниже.
julia> bar() = 1 bar (generic function with 1 method) julia> remotecall_fetch(bar, 2) ERROR: On worker 2: UndefVarError: #bar not defined [...] julia> anon_bar = ()->1 (::#21) (generic function with 1 method) julia> remotecall_fetch(anon_bar, 2) 1
Пакеты и Модули
В чем разница между "using" и "import"?
Разница всего одна, и на первый взгляд (с точки зрения синтаксиса) она может показаться очень незначительной. Разница между using и import заключается в том, что при использовании using вам нужно указать function Foo.bar(.., чтобы расширить функцию bar модуля Foo новым методом, а с import Foo.bar, вам нужно только указать function bar(..., и он автоматически расширит функцию bar модуля Foo.
Причина, по которой это достаточно важно, чтобы иметь отдельный синтаксис, заключается в том, что вы не хотите случайно расширять функцию, о существовании которой вы не знали, так как это может легко привести к ошибке. Это наиболее вероятно произойдет с методом, который принимает общий тип, например, строку или целое число, потому что и вы, и другой модуль могут определить метод для обработки такого общего типа. Если вы используете import, то вы заменяете реализацию bar(s::AbstractString) другого модуля своей новой реализацией, которая может легко делать совершенно другое (и сломать все/многие будущие использования других функций в модуле Foo, которые зависят от вызова bar).
Ничто и отсутствующие значения
Как работают "null", "ничто" или "отсутствие значения" в Julia?
В отличие от многих языков (например, C и Java), объекты Julia по умолчанию не могут быть "null". При попытке обратиться к несформированному элементу (переменная, поле объекта или элемент массива) немедленно вызывается ошибка. Эту ситуацию можно обнаружить с помощью функций isdefined или isassigned.
Некоторые функции используются только для побочных эффектов и не требуют возврата значения. В этих случаях принято возвращать значение nothing, которое является просто одиночным объектом типа Nothing. Это обычный тип без полей; в нём нет ничего особенного, кроме этой конвенции, и того, что REPL не печатает для него ничего. Некоторые языковые конструкции, которые в противном случае не имели бы значения, также возвращают nothing, например if false; end.
В ситуациях, когда значение x типа T существует не всегда, можно использовать тип Union{T, Nothing} для аргументов функций, полей объектов и типов элементов массивов, как эквивалент Nullable, Option или Maybe в других языках. Если само значение может быть nothing (в частности, когда T равно Any ), то более подходящим типом является Union{Some{T}, Nothing}, так как x == nothing тогда указывает на отсутствие значения, а x == Some(nothing) указывает на наличие значения, равного nothing. Функция something позволяет разворачивать объекты Some и использовать значение по умолчанию вместо nothing аргументов. Обратите внимание, что компилятор способен генерировать эффективный код при работе с Union{T, Nothing} аргументами или полями.
Для представления пропущенных данных в статистическом смысле (NA в R или NULL в SQL) используйте объект missing. Подробнее см. раздел Missing Values.
Пустой кортеж (()) также является формой ничто. Но его следует рассматривать не как ничто, а как кортеж из нулевых значений.
Пустой (или "нижний") тип, записываемый как Union{} (пустой тип объединения), — это тип без значений и без подтипов (кроме самого себя). Обычно вам не нужно использовать этот тип.
Память
Почему x += y выделяет память, когда x и y являются массивами?
В Julia x += y заменяется во время парсинга на x = x + y. Для массивов это означает, что вместо хранения результата в том же месте памяти, что и x, выделяется новый массив для хранения результата.
Хотя такое поведение может удивить некоторых, выбор сделан осознанно. Основная причина — наличие неизменяемых объектов в Julia, которые не могут изменить своё значение после создания. Действительно, число — неизменяемый объект; операторы x = 5; x += 1 не изменяют значение 5, они изменяют значение, связанное с x. Для неизменяемого объекта единственный способ изменить значение — переназначить его.
Для большей ясности рассмотрим следующую функцию:
function power_by_squaring(x, n::Int)
ispow2(n) || error("This implementation only works for powers of 2")
while n >= 2
x *= x
n >>= 1
end
x
end
После вызова, такого как x = 5; y = power_by_squaring(x, 4), вы получите ожидаемый результат: x == 5 && y == 625. Однако предположим теперь, что *=, при использовании с матрицами, вместо этого изменяет левую часть. Возникнут две проблемы:
- Для общих квадратных матриц
A = A*Bне может быть реализовано без временного хранения:A[1,1]вычисляется и хранится в левой части до того, как вы закончите его использовать в правой части. - Предположим, что вы готовы выделить временное хранилище для вычислений (что лишит большей части смысла использования
*=непосредственно); если вы воспользуетесь изменяемостьюx, то эта функция будет работать по-разному для изменяемых и неизменяемых входных данных. В частности, для неизменяемыхxпосле вызова у вас (в общем случае) будетy != x, а для изменяемыхxу вас будетy == x.
Поскольку поддержка обобщённого программирования считается более важной, чем потенциальные оптимизации производительности, которые могут быть достигнуты другими средствами (например, использование явных циклов), операторы, такие как += и *= работают путём переназначения новых значений.
Асинхронный ввод-вывод и одновременные синхронные записи
Почему одновременные записи в один и тот же поток приводят к перемешанному выводу?
Хотя API потокового ввода-вывода является синхронным, его реализация полностью асинхронна.
Рассмотрим вывод, напечатанный в следующем примере:
julia> @sync for i in 1:3
@async write(stdout, string(i), " Foo ", " Bar ")
end
123 Foo Foo Foo Bar Bar Bar
Это происходит потому, что, хотя вызов write синхронный, запись каждого аргумента уступает другим задачам, ожидая завершения части ввода-вывода.
print и println «запирают» поток во время вызова. Следовательно, изменение write на println в приведённом выше примере приводит к:
julia> @sync for i in 1:3
@async println(stdout, string(i), " Foo ", " Bar ")
end
1 Foo Bar
2 Foo Bar
3 Foo Bar
Вы можете заблокировать свои записи с помощью ReentrantLock следующим образом:
julia> l = ReentrantLock()
ReentrantLock(nothing, Condition(Any[]), 0)
julia> @sync for i in 1:3
@async begin
lock(l)
try
write(stdout, string(i), " Foo ", " Bar ")
finally
unlock(l)
end
end
end
1 Foo Bar 2 Foo Bar 3 Foo Bar
Массивы
В чём разница между нулевомерными массивами и скалярами?
Нулевомерные массивы представляют собой массивы вида Array{T,0}. Они ведут себя подобно скалярам, но есть важные различия. Они заслуживают особого упоминания, потому что являются особым случаем, который логично вытекает из общего определения массивов, но может показаться несколько неинтуитивным с первого взгляда. Следующая строка определяет нулевомерный массив:
julia> A = zeros()
0-dimensional Array{Float64,0}:
0.0
В этом примере A — это изменяемый контейнер, который содержит один элемент, который можно установить с помощью A[] = 1.0 и получить с помощью A[]. Все нулевомерные массивы имеют одинаковый размер (size(A) == ()) и длину (length(A) == 1). В частности, нулевомерные массивы не пустые. Если вам это кажется неинтуитивным, вот несколько идей, которые могут помочь понять определение массивов в Julia.
- Нулевомерные массивы — это «точка» к «линии» вектора и «плоскости» матрицы. Так же, как у линии нет площади (но она всё равно представляет собой набор вещей), у точки нет длины или каких-либо измерений (но она всё равно представляет собой вещь).
- Мы определяем
prod(())как 1, а общее количество элементов в массиве — это произведение размеров. Размер нулевомерного массива —(), и, следовательно, его длина —1. - Нулевомерные массивы не имеют встроенных измерений, в которые можно индексировать — они просто
A[]. Мы можем применить то же правило «последующей единицы» для них, как и для всех других размерностей массивов, поэтому вы можете индексировать их какA[1],A[1,1], и так далее.
Также важно понимать различия с обычными скалярами. Скаляры не являются изменяемыми контейнерами (хотя они итерируемы и определяют такие вещи, как length, getindex, например 1[] == 1). В частности, если x = 0.0 определён как скаляр, попытка изменить его значение с помощью x[] = 1.0 является ошибкой. Скаляр x может быть преобразован в нулевомерный массив, содержащий его, с помощью fill(x), и наоборот, нулевомерный массив a может быть преобразован в содержащийся в нём скаляр с помощью a[]. Ещё одно различие заключается в том, что скаляр может участвовать в операциях линейной алгебры, таких как 2 * rand(2,2), но аналогичная операция с нулевомерным массивом fill(2) * rand(2,2) является ошибкой.
Версии Julia
Хотите ли вы использовать релизную, бета- или ночную версию Julia?
Вы можете предпочесть релизную версию Julia, если вам нужна стабильная среда кода. Релизы обычно происходят каждые шесть месяцев, предоставляя стабильную платформу для написания кода.
Вы можете предпочесть бета-версию Julia, если не против отставать от последних исправлений ошибок и изменений, но вам нравится несколько более быстрое темпо изменений. Кроме того, эти двоичные файлы проверяются перед публикацией, чтобы гарантировать их полную работоспособность.
Вы можете предпочесть ночную версию Julia, если хотите воспользоваться последними обновлениями языка и не возражаете, если версия, доступная сегодня, иногда не работает.
Наконец, вы также можете рассмотреть возможность сборки Julia из исходного кода. Этот вариант предназначен, в основном, для тех, кто комфортно работает с командной строкой или интересуется изучением. Если это про вас, вы также можете быть заинтересованы в чтении наших руководств по участию.
Ссылки на каждый из этих типов загрузок можно найти на странице загрузки по адресу https://julialang.org/downloads/. Обратите внимание, что не все версии Julia доступны для всех платформ.
Когда удаляются устаревшие функции?
Устаревшие функции удаляются после последующего релиза. Например, функции, помеченные как устаревшие в релизе 0.1, больше не будут доступны начиная с релиза 0.2.
© 2009–2019 Jeff Bezanson, Stefan Karpinski, Viral B. Shah, and other contributors
Licensed under the MIT License.
https://docs.julialang.org/en/v1.0.4/manual/faq/