Spec-Zone.ru › Julia 1.1

Часто задаваемые вопросы

Сессии и REPL

Как удалить объект в памяти?

В Julia нет аналога функции clear из MATLAB; после определения имени в сессии 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.

Как перехватить CTRL-C в скрипте?

Запуск скрипта Julia с помощью julia file.jl не вызывает InterruptException, когда вы пытаетесь завершить его с помощью CTRL-C (SIGINT). Чтобы запустить определенный код перед завершением скрипта Julia, который может или не может быть вызван CTRL-C, используйте atexit. В качестве альтернативы вы можете использовать julia -e 'include(popfirst!(ARGS))' file.jl для выполнения скрипта, имея возможность перехватить InterruptException в блоке try.

Как передать параметры julia с помощью #!/usr/bin/env?

Передача параметров julia в так называемом shebang, например, #!/usr/bin/env julia --startup-file=no, может не работать на некоторых платформах, таких как Linux. Это связано с тем, что обработка аргументов в shebang зависит от платформы и не определена однозначно. В среде наподобие Unix надёжный способ передачи параметров julia в исполняемом скрипте — запустить скрипт как скрипт bash и использовать exec для замены процесса на julia:

#!/bin/bash
#=
exec julia --color=yes --startup-file=no -e 'include(popfirst!(ARGS))' \
    "${BASH_SOURCE[0]}" "$@"
=#

@show ARGS  # put any Julia code here

В приведенном выше примере код между #= и =# выполняется как скрипт bash. Julia игнорирует эту часть, поскольку это многострочный комментарий для Julia. Код Julia после =# игнорируется bash, поскольку он останавливает разбор файла, достигнув инструкции exec.

Функции

Я передал аргумент 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. Если вы хотите импортировать модуль, но использовать его символы только внутри определённой функции или набора функций, у вас есть два варианта:

  1. Используйте import:

    import Foo
    function bar(...)
        # ... refer to Foo symbols via Foo.baz ...
    end

    Это загружает модуль Foo и определяет переменную Foo, которая ссылается на модуль, но не импортирует другие символы из модуля в текущее пространство имён. Вы обращаетесь к символам Foo по их полному имени Foo.bar и т. д.

  2. Оборачивайте функцию в модуль:

    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

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

END_OF_DOCUMENT_MARKER

Одним из вариантов является проверка каждого целочисленного оператора на переполнение и повышение результатов до типов больших целых чисел, таких как 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

Почему Julia использует * для конкатенации строк? Почему не + или что-то ещё?

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

Обратите внимание, что вы также можете использовать string(...) для конкатенации строк (и других значений, преобразованных в строки); аналогично, repeat можно использовать вместо ^ для повторения строк. Синтаксис вставки также полезен для построения строк.

Пакеты и модули

В чём разница между "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, если ищете стабильную базу кода. Выпуски обычно происходят каждые 6 месяцев, предоставляя вам стабильную платформу для написания кода.

Вы можете предпочесть бета-версию 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.1.1/manual/faq/

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API