Julia: У широковещательной передачи была одна задача (например, широковещательная передача через итераторы и генератор)

Созданный на 21 сент. 2016  ·  69Комментарии  ·  Источник: JuliaLang/julia

Было удивительно обнаружить, что broadcast не работает с итераторами.

dict = Dict(:a => 1, :b =>2)
<strong i="7">@show</strong> string.(keys(dict)) # => Expected ["a", "b"]
"Symbol[:a,:b]"

Это связано с тем, что Broadcast.containertype возвращает Any https://github.com/JuliaLang/julia/blob/413ed79ec54f3a754ac8bc57c1d29835d17bd274/base/broadcast.jl#L31
приводит к откату по адресу: https://github.com/JuliaLang/julia/blob/413ed79ec54f3a754ac8bc57c1d29835d17bd274/base/broadcast.jl#L265

Определение containertype как Array для этого итератора приводит к проблемам с вызовом size на нем, потому что broadcast не проверяет интерфейс итератора iteratorsize(IterType) .

map решает эту проблему с помощью запасного варианта map(f, A) = collect(Generator(f,A)) который может быть более разумным, чем текущее определение broadcast(f, Any, A) = f(A)

broadcast

Самый полезный комментарий

Это сделано намеренно. broadcast предназначен для контейнеров с фигурами и по умолчанию обрабатывает объекты как скаляры. map предназначен для контейнеров без фигур и по умолчанию обрабатывает объекты как итераторы.

Например, broadcast обрабатывает строки как "скаляры", тогда как map выполняет итерацию по символам.

Все 69 Комментарий

Это сделано намеренно. broadcast предназначен для контейнеров с фигурами и по умолчанию обрабатывает объекты как скаляры. map предназначен для контейнеров без фигур и по умолчанию обрабатывает объекты как итераторы.

Например, broadcast обрабатывает строки как "скаляры", тогда как map выполняет итерацию по символам.

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

Кроме того, как ранее указывал map и broadcast , если нет, то какой смысл иметь оба.

@stevengj Но итераторы имеют форму (особенно генераторы) http://docs.julialang.org/en/release-0.5/manual/interfaces/#interfaces

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

@pabloferz Основное различие между map и broadcast - это обработка скаляров. Теперь определение скаляра является спорным, и я бы сказал, что все, что имеет length(x) > 1 не должно считаться скаляром.

Отметка, какие аргументы должны рассматриваться как итерируемые, вместо самого вызова функции, устранит двусмысленность. Думаю?

Для broadcast (я верю также в целом) наличие формы означает наличие size (а не только length ) и возможность индексации. За исключением кортежей все остальное без size рассматривается как скаляр. Учитывая текущую реализацию, вам сначала понадобится getindex или возможность определить его для объекта, который вы хотите транслировать. Для итераторов это вообще невозможно.

Я тоже столкнулся с этим. Исходя из # 16769, где я ищу способ fill! массива с повторяющимися вычислениями функции (вместо фиксированного значения), я подумал, что точечный синтаксис уже может помочь. Но когда a = zeros(2, 3); a .= [rand() for i=1:2, j=1:3] работает, (было бы) дешевле a .= (rand() for i=1:2, j=1:3) не работает; этот генератор - HasShape() , но действительно не имеет возможности индексирования. Я очень мало понимаю, как работает широковещательный / точечный синтаксис, но поможет ли здесь наличие черты для возможностей индексирования? для этого уже есть PR (# 22489) ...

@rfourquet , ты можешь сделать a = zeros(2, 3); a .= rand.()

Да, но мне следовало быть более точным: я хочу использовать функцию, которая получает индексы в качестве параметров, например a .= (f(i, j) for i=1:2, j=1:3) .

Каковы будут недостатки широковещательной передачи размеров итераторов HasShape ? Звучит как естественный поступок.

@nalimilan , на первый взгляд, я думаю, что это было бы разумно и, вероятно, относительно легко реализовать. Было бы нарушением, так что нужно сделать 1.0.

Одна потенциальная проблема заключается в том, что итераторы HasShape не обязательно поддерживают getindex , и это может затруднить реализацию?

Одна из возможностей - временно (для 1.0) сделать простую реализацию, которая просто скопирована в массив. Это позволит опубликовать оптимизацию 1.0

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

Как я сказал выше, у меня есть PR на # 22489, чтобы разрешить индексацию в итераторы, если это может помочь.

Что нужно сделать для 1.0, чтобы хотя бы улучшить поведение в 1.x?

Спасибо @nalimilan за то, что поднял этот вопрос, я тоже хотел это сделать. Если разрешение генераторов HasShape в правой части широковещательного выражения невозможно реализовать для 1.0, должны ли мы сделать это сейчас ошибкой вместо того, чтобы рассматривать генераторы как скаляры? так что это можно было включить в 1.x.

: +1: Triage рекомендует сделать это ошибкой (безопасный вариант) или вызвать collect (если это легко сделать).

map рассматривает все свои аргументы как контейнеры и пытается перебрать их все. В моем идеальном мире broadcast был бы аналогичен и обрабатывал бы все его аргументы, имеющие формы, которые могут транслироваться, и выдавал бы ошибку, если, например, size не определено. Я отмечу, что любое значение можно рассматривать как скаляр в широковещательной передаче, заключив его в fill , получив массив 0-d:

julia> fill("a")
0-dimensional Array{String,0}:
"a"

julia> fill([2])
0-dimensional Array{Array{Int64,1},0}:
[2]

Вы действительно предлагаете рассматривать все скаляры как контейнеры по умолчанию? Это звучит не очень практично.

Глядя на то, как мы могли бы поддерживать любые итерации или просто выдавать для них ошибку, пока мы их не поддержим, похоже, что нам понадобится способ идентификации итераторов в BroadcastStyle . В настоящее время это невозможно, поскольку Base.iteratorsize возвращает HasLength даже для скаляров, таких как Symbol . Мы могли бы ввести черту Base.isiterable (которая может быть полезна для других целей) или сделать Base.iteratorsize умолчанию равным NotIterable (что также имеет смысл, если иметь HasLength по умолчанию всегда звучит немного удивительно, хотя и безобидно).

(Сложный случай для дальнейшего обсуждения: UniformScaling .)

@timholy Поскольку вы выполнили редизайн broadcast , есть ли предложения?

@JeffBezanson , весь смысл broadcast состоит в том, чтобы иметь возможность "транслировать" скаляры для соответствия контейнерам, например, для выполнения ["bug", "cow", "house"] .* "s" ----> ["bugs", "cows", "houses"] . Это принципиально отличается от поведения map .

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

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

В несвязанном PR (https://github.com/JuliaLang/julia/pull/25339) @Keno предложил использовать applicable(start, (x,)) чтобы узнать, является ли x повторяющимся или нет. Должны ли мы использовать здесь тот же подход? Я бы посчитал более ясным иметь более явное определение итераторов (основанное либо на Base.iteratorsize или на признаке), но использование start тоже имеет смысл.

У нас может быть явный признак, который по умолчанию равен applicable(start, (x,)) ; это позволит при необходимости отменить его.

Я подал № 25356, чтобы проиллюстрировать возможные решения и их недостатки.

Из примера @stevengj ["bug", "cow", "house"] .* "s" ----> ["bugs", "cows", "houses"] кажется, что итерации недостаточно, поскольку строки итерируемы, но действуют как скаляры. Если вам все равно нужно определить черту, может быть лучше продолжить требовать согласия для широковещательной передачи, а не добавлять требования ко всем итераторам.

К счастью, keys(dict) теперь возвращает AbstractSet , поэтому, если мы добавим черту широковещания для AbstractSet это исправит пример в OP. Мы также могли бы добавить ошибку для трансляции Generator чтобы отловить некоторые распространенные случаи.

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

Да, при подготовке PR я понял, что наборы действительно не лучший пример итераторов, которые должны поддерживать трансляцию. Такие вещи, как Generator и ProductIterator - гораздо более интересные случаи.

Может быть, ответ состоит в том, чтобы (попытаться) транслировать итераторы с HasShape и продолжать рассматривать все остальное как скаляры? Не исправит OP, но в остальном довольно элегантно.

Другая случайная мысль: может быть, передача более одного аргумента (как в string.(x) ) должна быть особым случаем, который больше похож на map , поскольку совместимость форм не является проблемой?

Может быть, ответ состоит в том, чтобы (попытаться) транслировать итераторы, у которых есть HasShape, и продолжать рассматривать все остальное как скаляры? Не исправит OP, но в остальном довольно элегантно.

Я не уверен, что у нас есть веские основания для исключения итераторов HasLength . Мы поддерживаем широковещательную передачу по кортежам (которые не реализуют size ), так почему бы не рассматривать бесформенные итераторы как кортежи? Например, было бы разумно использовать результат keys(::OrderedDict) с broadcast . Если мы его не поддерживаем, у людей возникнет соблазн определить свои итераторы как HasShape просто для того, чтобы их можно было использовать с broadcast (и красивым синтаксисом с точками).

Цитируя Стива,

broadcast предназначен для контейнеров с фигурами.

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

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

Может быть, @stevengj сможет понять, почему он думает, что broadcast должен поддерживать только контейнеры с формой? Вы бы поддержали рассмотрение кортежей как скаляров?

Я думаю, что причина для обработки кортежей как контейнеров в broadcast (# 16986) заключалась в том, что на практике они часто используются как по существу статические векторы, и рассматривать их как "скаляры" в broadcast просто нельзя. в любом случае очень полезно. Напротив, строки (a) часто рассматриваются как «атомы» для операций обработки строк и (b) вообще не имеют последовательной индексации, поэтому они очень плохо вписываются в структуру broadcast .

В принципе, я бы поддержал использование итераторов HasShape качестве контейнеров в broadcast . Основная проблема, как я отмечал выше, заключается в том, что наличие HasShape не гарантирует, что getindex работает.

Основная проблема, как я отмечал выше, заключается в том, что наличие HasShape не гарантирует, что getindex работает.

Может ли что-то вроде # 22489 помочь, т.е. иметь свойство итератора, которое указывает, является ли итератор индексируемым?

Может ли что-то вроде # 22489 помочь, т.е. иметь свойство итератора, которое указывает, является ли итератор индексируемым?

Но тогда будут поддерживаться только индексируемые итераторы с broadcast ? Это звучит слишком ограничительно, поскольку было бы очень полезно иметь возможность делать такие вещи, как string.(itr, "1") для любой итерации (например, результат keys(::OrderedDict) ), и для реализации этого не требуется индексация. Я думаю, нам лучше выдать ошибку для всех итераторов, которые не поддерживают индексацию в 0.7 / 1.0, и попытаться поддержать их в последующих выпусках. В любом случае рассматривать итераторы как скаляры не очень полезно. Затем мы можем реализовать любое поведение, которое захотим, в версиях 1.x.

@stevengj Я согласен с вашими аргументами относительно строк и кортежей, но почему бы нам не рассматривать итераторы HasLength как кортежи? Я до сих пор не читал оправдания этому.

@nalimilan , я склонен думать, что broadcast должны поддерживать только итераторы indexable + hasshape. Попытка втиснуть в эту функцию общие итераторы слишком сильно сбивает с толку - в какой-то момент вам следует просто использовать map .

было бы очень полезно делать что-то string.(itr, "1") для любой итерации ... В любом случае, рассматривать итераторы как скаляры не очень полезно.

Случай со строками противоречит этому - сам аргумент "1" повторяется в вашем примере. Тонны вещей можно повторять (например, PyObject s в PyCall определяют start и т. Д.), Включая такие вещи, как неупорядоченные наборы, где концепция broadcast действительно нарушена.

Также обратите внимание, что # 24990 сделает map даже проще, чем сейчас, например, вы сможете выполнить map(string(_,"1"), itr) .

@nalimilan , я склонен думать, что широковещательной передачей должны поддерживаться только итераторы indexable + hasshape. Попытка втиснуть в эту функцию общие итераторы слишком сильно сбивает с толку - в какой-то момент вам следует просто использовать map.

В настоящее время у нас нет трейта для индексируемых итераторов. Как бы вы посоветовали это передать? Мой WIP PR # 25356 выдает ошибку для итераторов, которые не поддерживают индексацию, что звучит неплохо, если предположить, что не очень полезно рассматривать итераторы как скаляры. Если мы хотим рассматривать их как скаляры, нам понадобится еще одна черта, верно?

Я склонен выдвигать ошибки для всех случаев, которые не совсем очевидны, чтобы мы могли реализовать любое поведение в будущем, вместо того, чтобы блокировать себя поведением по умолчанию, которое не обязательно очень полезно (например, обработка некоторых итераторов как скаляров) . Как показывает эта проблема, для правильного проектирования поведения broadcast требуется время.

(FWIW, PyObject мне не кажется отличным примером, поскольку IIUC реализует протокол итераций только потому, что заранее не знает, будет ли он обертывать итератор Python или нет. PyObject явно является здесь исключением, точно так же, как ему нужно использовать перегрузку getfield чтобы выглядеть как стандартный объект Julia. Наборы - это более юлианский пример.)

Мы могли бы добавить трейт для индексируемых итераторов HasShape , как предлагалось в другом месте.

Triage нравится идея сделать широковещательную итерацию по всем аргументам (например, map) и добавить символ оператора (например, выполнить const & = Ref как было предложено ранее в другом выпуске, или, возможно, ~ ), чтобы явно отметить 0-d аргументы.

@vtjnash , что это вообще значит для отличного от HasShape ? Вы имеете в виду, что хотите, чтобы широковещательная передача перебирала такие вещи, как строки и наборы? Текущая реализация broadcast тесно связана с getindex ... думали ли вы о том, как реализовать ее без getindex , особенно для объединения аргументов разной размерности?

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

Похоже, что черта IteratorAccess из PR https://github.com/JuliaLang/julia/pull/22489 может быть адаптирована / повторно использована для обнаружения индексируемых итераторов. Для https://github.com/JuliaLang/julia/pull/24774 также необходимо знать, какие итераторы являются индексируемыми (и, следовательно, должны реализовывать keys ) .

Копия: @rfourquet

👍 Triage рекомендует сделать это ошибкой (безопасный выбор) или вызвать для нее сборку (если это легко сделать).

Может ли сортировка выбрать конкретную стратегию для принятия здесь? Например, что "это" в комментарии @JeffBezanson выше? Должны ли мы выдавать ошибки для всех итераторов, которые не поддерживают индексацию (самый безопасный вариант на данный момент, чтобы мы могли делать все, что захотим позже), или мы должны рассматривать некоторые итераторы как скаляры? Следует ли добавить признак для индексируемых итераторов, и если да, то в какой форме (новый признак или новый вариант для Base.IteratorSize )? Должны ли мы вообще добавить трейт для итераторов (чтобы мы могли отличить их от скаляров)?

Следующее поведение кажется хорошим:

  • По умолчанию старайтесь повторять и транслировать каждый аргумент.
  • Выдайте ошибку, если это не сработает по какой-либо причине.
  • Передайте Ref(x) или [x] чтобы заставить x обрабатываться как скаляр.
  • Добавьте признак, который можно определить, чтобы новый тип обрабатывался как скаляр, а не выдавал ошибку. Обратите внимание, что это не следует использовать для выбора между повторением и отсутствием повторения. Просто чтобы превратить ошибку в скалярное поведение.

Не могли бы вы пояснить примечание по последнему пункту (возможно, на примере)? Я не уверен, что это означает, что признак существует, но не может использоваться для выбора между повторением и отсутствием повторения.

Таким образом, в основном «попытаться перебрать и передать каждый аргумент» подразумевает, что нам нужно определить BroadcastStyle для возврата Scalar() для всех типов, не являющихся коллекциями (в частности, Number , Symbol и AbstractString )? Похоже на "черту", упомянутую в последнем пункте.

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

Похоже на "черту", упомянутую в последнем пункте

Нет, последний пункт означает, что если что-то реализует итерацию, то широковещательная передача выполняет итерацию - свойство Scalar исчезнет. Для некоторых общих явно не повторяемых типов (например, подтипов Type и Function ) нам может потребоваться свойство NotIterable которое превращает MethodError в итерацию, которая производит одно значение (этот объект). Я вообще не помню, зачем это было нужно.

Итак, в основном «попытаться перебрать и передать каждый аргумент» означает, что нам нужно определить BroadcastStyle для возврата Scalar () для всех типов, не являющихся коллекциями (особенно Number, Symbol и AbstractString)? Похоже на "черту", упомянутую в последнем пункте.

Нет, все скалярные подтипы Number повторяются сами по себе, и поэтому все в порядке. Нам нужно определить его для символа. AbstractString будет работать как коллекция.

Мне не нравится любой дизайн, который требует от нас определения метода для типа, который будет рассматриваться как скаляр. Это должно быть по умолчанию. Я также не думаю, что строки следует рассматривать как контейнеры для широковещательной передачи.

Я по-прежнему считаю, что широковещательная передача должна рассматривать как контейнеры только итераторы HasShape; это согласуется с дизайном трансляции с самого начала. Что не так с этим?

Проблема с этим в OP; если у вас есть итератор без формы, рассмотрение его как скаляра даст безумный ответ.

Кроме того, я был бы совершенно счастлив отказаться от части предложения о «характеристиках». Никто не жалуется на

julia> map(string, [1,2], :a)
ERROR: MethodError: no method matching start(::Symbol)

Возможно, причина, по которой результат в OP является неожиданным, заключается в том, что никто на самом деле не предполагает, что широковещательный вызов рассматривает _все_ аргументы как скаляры; если есть только один аргумент и вообще есть способ рассматривать его как коллекцию / итератор, это почти наверняка то, что намеревается пользователем. Хотя конечно 1 .+ 1 должен продолжать работать?

Мне это пришло в голову, но мне кажется затруднительным рассматривать один аргумент в качестве особого случая.

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

Я по-прежнему считаю, что широковещательная передача должна рассматривать как контейнеры только итераторы HasShape; это согласуется с дизайном трансляции с самого начала. Что не так с этим?

@stevengj Что не так, IMHO, это то, что некоторые операции не работают, когда может быть реализовано совершенно разумное поведение: относитесь к итераторам HasLength точно так же, как Tuple , который в настоящее время имеет специальный регистр. Даже если мы не будем поддерживать их прямо сейчас, я хотел бы оставить возможность поддержать их в какой-то момент в 1.x.

Никто не жалуется на

julia> map (строка, [1,2],: a)
ОШИБКА: MethodError: нет метода, соответствующего start (:: Symbol)

@JeffBezanson OTC, я поддерживаю текущее поведение broadcast , которое повторяет :a мере необходимости. Такие вещи могут быть очень полезны, например, для переименования серии столбцов DataFrame . Вы предлагаете изменить broadcast чтобы выдавать ошибку типа map ?

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

Это просто, но довольно неудобно. Я согласен с @stevengj, что скаляры должны транслироваться по умолчанию, а не вызывать ошибку. Конечно, поскольку типы Number повторяются, раздражение не всегда будет заметно, но, как показывает пример Symbol в целом это не будет очень полезно. Char будет другим, и многие пользовательские типы, определенные в package, также пострадают от этого (и в конечном итоге их BroadcastStyle как Scalar() ).

Я думаю, что суть проблемы в том, что у нас нет признака, позволяющего отличать коллекции от скаляров. Таким образом, наиболее прямым решением было рассматривать итераторы HasShape как коллекции, а другие типы - как скаляры (включая итераторы HasLength поскольку это значение по умолчанию для всех типов). Лично я думаю, что введение трейта для коллекций / итераций имело бы большой смысл, но если мы не готовы к этому и если мы не можем полагаться на определение start для обнаружения итераций, я боюсь, что нам придется сохранить текущее поведение.

В предложении Джеффа в https://github.com/JuliaLang/julia/issues/18618#issuecomment -360594955 есть место для широковещательной передачи, рассматривающей Symbol и Char как "скалярные" типы - они просто нужно подписаться на поведение. По умолчанию они будут ошибкой, поскольку они не реализуют start .

Самая привлекательная часть здесь заключается в том, что единственное разумное определение типа «коллекция» - это то, что он является итеративным. Да, это означает, что строки - это коллекции. Иногда их используют как таковые! Итак, давайте по умолчанию выберем поведение, которое легко позволяет людям подключаться к другому абоненту на месте звонка.

Но здесь есть бородавка. Поскольку числа являются итеративными (они даже HasShape ), они будут рассматриваться как нульмерные контейнеры. Это означает, что, доведя до логического завершения, 1 .+ 2 != 3 . Вместо этого было бы fill(3, ()) .

РЕДАКТИРОВАТЬ: в попытке избежать дальнейшего срыва потока, перешел к беседе:

https://discourse.julialang.org/t/lazycall-again-sorry/8629

В предложении Джеффа в # 18618 (комментарий) есть место, чтобы разрешить широковещательную передачу символов и Char как «скалярных» типов - им просто нужно согласиться на поведение. По умолчанию они будут ошибкой, так как не реализуют запуск.

Да, моя позиция основана на предположении, что скаляры - это более естественный запасной вариант, особенно с учетом того, что коллекции должны реализовывать некоторые методы (итерацию, возможно, индексацию), в то время как скаляры - это просто «остальное» и не имеют ничего общего. В конце концов, любой тип сможет реализовать любое поведение, которое он хочет, но мы должны сделать его максимально удобным и логичным, что, в частности, должно помочь избежать несоответствий (например, некоторые типы объявлены и ведут себя как скаляры, а другие нет).

Я не особо беспокоюсь о том, чтобы иметь несколько исключений для основных типов, таких как строки и числа, если правила ясны для других типов.

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

  • map может быть сильно привязан к протоколу итераций - кажется верным, что мы можем сделать ленивый out = map(f, iterable) для любого произвольного iterable , например, first(out) то же самое, что и f(first(iterable)) , и мне кажется, что эта универсальная ленивая операция может быть полезной.
  • broadcast может быть сильно привязан к интерфейсу индексирования - кажется правильным, что мы можем сделать ленивый out = broadcast(f, indexable) такой, что out[i] будет таким же, как f(indexable[i]) , и мне кажется, что эта типовая ленивая операция может быть полезна. Очевидно, что broadcast с несколькими входами по-прежнему может делать все те модные вещи, которые он делает сейчас. Для широковещательной передачи скаляры - это те вещи, которые нельзя проиндексировать (или просто проиндексировать, например, Number и Ref и AbstractArray{0} ).

Я также считаю, что было бы желательно, чтобы map с одним аргументом и broadcast одним аргументом делали очень похожие вещи для коллекций, которые являются как итерабельными, так и индексируемыми. Однако тот факт, что итерация AbstractDict возвращает разные вещи, чем getindex похоже, препятствует хорошей унификации. : frowning_face: (Другие наши типы коллекций кажутся прекрасными)

(Для меня тот факт, что такие вещи, как строки, возможно, должны быть явно обернуты, как ["bug", "cow", "house"] .* ("s",) здесь не является нарушением сделки. У меня такая же проблема, когда я хочу думать о 3-векторе как «единую трехмерную точку», и с ней не так уж сложно иметь дело (xref # 18379)).

Я согласен с тем, что broadcast должен быть для индексируемых контейнеров, но я думаю, что он должен быть последовательно индексируемым, что исключает строки. например, collect(eachindex("aαb🐨γz")) дает [1, 2, 4, 5, 9, 11] , что будет плохо работать с любой реализацией broadcast основанной на индексировании.

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

Я не уверен, что последовательные индексы - хорошее ограничение - например, словари будут иметь произвольные индексы.

Однако broadcast(f, ::String) не может создать новый String и гарантировать, что выходные индексы останутся такими же, как входные индексы, поскольку ширина символов UTF-8 может измениться в f ( он должен превратиться во что-то вроде AbstractDict{Int, Char} чтобы дать эту гарантию, что на самом деле не кажется очень полезным!). Я бы почти сказал, что индексы String больше похожи на «токены» для быстрого поиска, чем на семантически важные индексы (например, вы можете преобразовать в эквивалентную строку UTF-32, и индексы изменится).

Я не возражаю, если мы сделаем выбор поведения при трансляции через trait; Я просто говорю, что представление о том, как ведет себя общий broadcast(f, ::Any) является хорошим способом руководства реализацией таких вещей, как broadcast(f, ::AbstractDict) (и, естественно, ответит на вопрос, который я поднял в # 25904, т.е. словарные значения, а не пары ключ-значение).

Действительно ли люди довольны этим изменением? Мне, например, никогда не приходилось транслировать через контейнер без формы, в то время как я транслировал вещи, которые следует рассматривать как скаляры все время . Каждое предупреждение об устаревании, которое я «исправляю», заставляет меня плакать.

Я транслирую вещи, которые следует рассматривать как скаляры _все время_.

Что это за типы?

Может быть что угодно. Например, в пакете, который определяет тип модели оптимизации Model и тип переменной решения Variable , у вас может быть x::Vector{Variable} для которого вы хотите получить значения после решения модель model с использованием функции value(::Variable, ::Model)::Float64 . Раньше это можно было сделать так:

value.(x, model)

Также часто бывает, что типы аргументов взяты из других пакетов, поэтому добавление метода в broadcastable для этих типов будет в этом случае пиратством типов. Поэтому вам нужно использовать Ref или одноэлементный кортеж. Это не непреодолимо, но, на мой взгляд, это просто делает общий случай намного менее элегантным, чтобы поддерживать относительно неясный шаблон использования.

Да, я понимаю вашу точку зрения и согласен, что в подобных ситуациях это раздражает. Тем не менее, старое поведение было абсолютно проблематичным - оно было одним из тех, что «резервное копирование по умолчанию в некоторых случаях окончательно неверно».

Короче говоря, есть четыре варианта, позволяющих избежать неправильного отката:

  1. требовать _everything_ для реализации некоторого метода, описывающего, как они транслируют
  2. По умолчанию рассматривать вещи как контейнеры и ошибки / устаревшие для неконтейнеров.

    • Мы просто попробуем iterate неизвестных объектов, и это приведет к ошибке для скаляров.

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

  3. По умолчанию обработка вещей как скаляров и ошибок для неизвестных контейнеров

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

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

  4. Отметьте applicable(iterate, …) и соответствующим образом измените поведение

    • В настоящее время это не работает из-за механизма устаревания от start / next / done, и в целом может быть неправильным для типов оболочки, которые откладывают методы на член.

Вариант 1 хуже для всех, вариант 2 - это статус-кво, вариант 3 - наоборот, а вариант 4 - это то, что мы никогда раньше не делали и, вероятно, будет содержать ошибки.

Я предполагаю, что часть обсуждения, должно быть, происходила за кулисами, но меня просто не убеждают аргументы, которые я видел в этой ветке и в https://github.com/JuliaLang/julia/pull/25356 против nalimilan Позиции stevengj .

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

Это мое основное несогласие. Мне кажется, что во всем коде Юлии # of iterator types << # of types that should be treated as scalars in a broadcast situation < # of broadcast calls . Поэтому я бы предпочел, чтобы количество раз, которое нужно было сделать что-то «лишнее», зависело от количества типов итераторов, а не от количества широковещательных вызовов. И если автор библиотеки определяет итератор, вполне разумно просить их определить еще один метод, тогда как _ совершенно неразумно просить каждого автора пакета определить Base.broadcastable(x) = Ref(x) для всех их не повторяемых типов, чтобы избегайте уродливых (ИМХО) Ref s в большом проценте вызовов broadcast .

Я знаю , что , имея один метод для реализации , чтобы определить итерация хорошо, но это не значит, что много работы , чтобы осуществить еще одну для любого нового признака, или для этого требуется указать Base.iteratorsize для нового итератора (и избавление от проблемного HasLength default). Тогда резервный метод broadcastable может быть основан на этой характеристике. Или, если вы действительно любите определять итерацию с помощью одного метода, вы можете (после устаревания-удаления) использовать эту явную черту по умолчанию в applicable(iterate, ...) как в https://github.com/JuliaLang/ julia / issues / 18618 # issuecomment -354618742, и при необходимости просто переопределите это значение по умолчанию. Угловые случаи, такие как String также могут быть обработаны путем дальнейшей специализации broadcastable если это необходимо.

Фактически это дизайн версии 0.6, который привел к этой проблеме, а также № 26421, № 19577, № 23197, № 23746 и, возможно, больше - поискать это сложно.

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

Возможно, вы правы в том, что существует больше «скалярных» настраиваемых типов, чем подобных итератору, но я придерживаюсь того факта, что широковещательная передача - это, прежде всего, операция над контейнерами. Я ожидаю, что f.(x) выполнит какое-то сопоставление, а не будет просто f(x) .

И, наконец, контейнеры, которые получают скалярную обработку по умолчанию, просто не могут использоваться поэлементно с широковещательной рассылкой. Например, String - это тип коллекции, в котором мы сделали специальный регистр, чтобы вести себя как скаляр; невозможно «проникнуть внутрь» и работать поэлементно, даже если в некоторых ситуациях это имеет смысл (например, isletter.("a1b2c3") ). Это аргумент асимметрии: вы можете более эффективно обернуть контейнеры в Ref, чтобы рассматривать их как скаляры, чем вы можете collect их в фактически транслируемую коллекцию.

Это основные аргументы. Насчет безобразия Ref полностью согласен. Решение есть # 27608.

Справедливо. У меня нет никаких аргументов или волшебных решений этих проблем, и https://github.com/JuliaLang/julia/pull/27608 улучшит ситуацию.

@tkoolen У меня были те же проблемы и случай использования .

@mbauman Приведенные выше аргументы не могут быть полностью убедительными. Вот два вопроса для уточнения:

1) Можно было бы сделать broadcastable необходимым интерфейсом для любой итерации.
Это было бы полностью систематическим и заставило бы разработчиков задуматься о
как их итератор должен вести себя при трансляции.
Рекомендация установить его как collect(x) в большинстве случаев сделает переход относительно простым.
Никакой потери производительности не было бы, правда?

2) Таким образом, все сводится к желанию иметь ошибку для f.(x) если x транслируется как скаляр.
Почему не появляется предупреждение / ошибка линтера для f.(x, y, z) , например «все аргументы 'f' транслируются как скаляры»?

В любом случае, было бы разумно исправить # 27563 (например, # 27608) и позволить пользователям поиграть с ним некоторое время до 1.0.
[0.7 и 1.0.0-rc1.0 были выпущены без исправления].

В любом случае, было бы разумно исправить # 27563 (например, # 27608) и позволить пользователям поиграть с ним некоторое время до 1.0.
[0.7 и 1.0.0-rc1.0 были выпущены без исправления].

Я так понимаю, вы пропустили новость о выпуске 1.0 .

@StefanKarpinski действительно пропустил это. Поздравляем всех разработчиков, Юля потрясающая, продолжайте!

Была ли эта страница полезной?
0 / 5 - 0 рейтинги