Подтвердить что ты не робот

Почему работает политика NPM дублированных зависимостей?

По умолчанию, когда я использую NPM для управления пакетом в зависимости от foo и bar, оба из которых зависят от corelib, по умолчанию NPM будет устанавливать corelib дважды (один раз для foo и один раз для bar). Они могут быть даже разными версиями.

Теперь предположим, что corelib определил некоторую структуру данных (например, объект URL), которая передается между foo, bar и основным приложением. Теперь, что бы я ожидал, это если бы имелось когда-либо обратно несовместимое изменение этого объекта (например, одно из названий полей было изменено), а foo зависело от corelib-1.0 и bar зависело от corelib-2.0, я был бы очень sad panda: версия bar corelib-2.0 может отображаться в структуре данных, созданной старой версией corelib-1.0, и все будет работать не очень хорошо.

Я был очень удивлен, обнаружив, что эта ситуация в принципе никогда не бывает (я трал Google, Qaru и т.д., ища примеры людей, чьи приложения перестали работать, но кто мог бы исправить это, запустив dedupe.) Итак, мой вопрос is почему это так? Является ли это потому, что библиотеки node.js никогда не определяют структуры данных, которые совместно используются вне программ? Это потому, что разработчики node.js никогда не нарушают совместимость своих структур данных? Я действительно хотел бы знать!

4b9b3361

Ответ 1

Если я хорошо понимаю, предполагаемая проблема может быть:

  • Модуль A

    exports = require("c") //v0.1
    
  • Модуль B

    console.log(require("a"))
    console.log(require("c")) //v0.2
    
  • Модуль C

    • v0.1

      exports = "hello";
      
    • v0.2

      exports = "world";
      

Скопировав C_0.2 в node_modules и C0.1 в node_modules/a/node_modules и создав фиктивные пакеты. json, я думаю, что создал случай, о котором вы говорите.

будет иметь две разные конфликтующие версии C_data​​strong > ?

Краткий ответ:

. Итак, node не обрабатывает конфликтующие версии.

Причина, по которой вы не видите ее в Интернете, объясняется тем, что gustavohenke объясняет, что node, естественно, не поощряет вас загрязнять глобальную область или структуру цепочек прохождения между модулями.

Другими словами, не так часто вы увидите, что модуль экспортирует другую структуру модуля.

Ответ 2

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

В Haskell мы делаем инкапсуляцию на уровне модуля. Например, возьмите модуль, который определяет тип T и набор функций, и экспортирует конструктор типа T (но не его определение) и некоторые из функций. Обычный способ использования такого модуля (и единственный способ, который разрешает система типов), - использовать одну экспортированную функцию create для создания значения типа T и другую экспортированную функцию consume, чтобы использовать значение типа T: consume (create a b c) x y z.

Если у меня было две разные версии модуля с разными определениями T, и я смог использовать create из версии 1 вместе с consume из версии 2, то я, скорее всего, получаю сбой или неверный ответ. Обратите внимание, что это возможно, даже если публичный API и внешне наблюдаемое поведение двух версий идентичны; возможно, версия 2 имеет другое представление T, которое позволяет более эффективно реализовать consume. Конечно, система типов GHC не позволяет вам этого делать, но таких угроз на динамическом языке нет.

Вы можете перевести этот стиль программирования прямо на язык JavaScript или Python:

import M
result = M.consume(M.create(a, b, c), x, y, z)

и у вас будет такая же проблема, о которой вы говорите.

Тем не менее, гораздо чаще используется стиль OO:

import M
result = M.create(a, b, c).consume(x, y, z)

Обратите внимание, что из модуля импортируется только create. consume в некотором смысле импортируется из объекта, который мы вернули из create. В вашем примере foo/bar/corelib скажем, что foo (который зависит от corelib-1.0) вызывает create и передает результат в bar (который зависит от corelib-2.0), который будет называть consume на нем. На самом деле, в то время как foo требует зависимости от corelib для вызова create, bar не требует зависимости от corelib для вызова consume вообще. Он использует только понятия базового языка для вызова consume (что мы могли бы называть getattr в Python). В этой ситуации bar в конечном итоге вызовет версию consume из corelib-1.0, независимо от того, какая версия бар Corelib "зависит от".

Разумеется, для этого для работы публичный API-интерфейс corelib не должен слишком сильно меняться между corelib-1.0 и corelib-2.0. Если bar хочет использовать метод fancyconsume, который является новым в corelib-2.0, то он не будет присутствовать на объекте, созданном corelib-1.0. Тем не менее, эта ситуация намного лучше, чем в оригинальной версии Haskell, где даже изменения, которые вообще не влияют на публичный API, могут привести к поломке. И, возможно, бар зависит от возможностей corelib-2.0 для объектов, которые он создает и потребляет сам, но использует API corelib-1.0 для использования объектов, которые он получает извне.

Чтобы достичь чего-то подобного в Haskell, вы можете использовать этот перевод. Вместо прямого использования основной реализации

data TImpl = TImpl ...     -- private
create_ :: A -> B -> C -> TImpl
consume_ :: TImpl -> X -> Y -> Z -> R
...

мы завершаем интерфейс пользователя экзистенциальным в пакете API corelib-api:

module TInterface where

data T = forall a. T { impl :: a,
                       _consume :: a -> X -> Y -> Z -> R,
                       ... }   -- Or use a type class if preferred.
consume :: T -> X -> Y -> Z -> R
consume t = (_consume t) (impl t)

а затем реализация в отдельном пакете corelib:

module T where

import TInterface

data TImpl = TImpl ...     -- private
create_ :: A -> B -> C -> TImpl
consume_ :: TImpl -> X -> Y -> Z -> R
...

create :: A -> B -> C -> T
create a b c = T { impl = create_ a b c,
                   _consume = consume_ }

Теперь foo использует corelib-1.0 для вызова create, но для bar требуется только corelib-api для вызова consume. Тип T живет в corelib-api, поэтому, если общедоступная версия API не изменяется, тогда foo и bar могут взаимодействовать, даже если bar связан с другой версией corelib.

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

Ответ 3

эта ситуация в принципе никогда не бывает

Да, мой опыт действительно заключается в том, что это не проблема в экосистеме Node/JS. И я думаю, что это частично благодаря принципу надежности .

Ниже мой взгляд на то, почему и как.

Примитивы, ранние дни

Я думаю, что первая и самая главная причина заключается в том, что язык обеспечивает общую основу для примитивных типов (Number, String, Bool, Null, Undefined) и некоторых базовых составных типов (Object, Array, RegExp и т.д.)).

Итак, если я получаю String из одного из API-интерфейсов libs, которые я использую, и передаю его другому, это не может пойти не так, потому что существует только один тип String.

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

Не так в Haskell. Прежде чем я начал использовать stack, я несколько раз сталкивался с следующей ситуацией с Text и ByteString:

Couldn't match type ‘T.Text’
               with ‘Text’
NB: ‘T.Text’
      is defined in ‘Data.Text.Internal’ in package ‘text-1.2.2.1’
    ‘Text’ is defined in ‘Data.Text.Internal’ in package ‘text-1.2.2.0’
Expected type: String -> Text
  Actual type: String -> T.Text

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

В качестве примера это могло быть незначительным исправлением для функции intersperse, которая гарантировала выпуск 1.2.2.1. Что совершенно неуместно для меня, если все, о чем я забочусь, в этом гипотетическом примере, является конкатенирование некоторых Text и сравнение их length s.

Составные типы, объекты

Иногда есть достаточные основания для отклонения JS от встроенных типов данных: возьмите Promise в качестве примера. Это такая полезная абстракция по сравнению с асинхронными вычислениями по сравнению с обратными вызовами, которые многие API начали использовать. Что теперь? Почему мы не сталкиваемся с множеством несовместимостей, когда разные версии этих объектов {then(), fail(), ...} передаются, вниз и вокруг дерева зависимостей?

Я думаю, что благодаря принципу надежности .

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

Итак, если я создаю JS-библиотеку, которую я знаю, возвращает promises и принимает promises как часть своего API, я буду очень осторожен, как я взаимодействую с полученными объектами. Например. Я не буду называть притягательные .success(), .finally(), ['catch']() методы на нем, так как я хочу быть как можно более совместимым с разными пользователями, с различными реализациями Promise s. Итак, очень консервативно, я могу просто использовать .then(done, fail), и не более того. На данный момент не имеет значения, использует ли пользователь promises, который возвращает моя библиотека, или Bluebirds ', или даже если они вручную пишут свои собственные, если они придерживаются самой простой Promise законы "- самые основные контракты API.

Может ли это привести к поломке во время выполнения? Да, оно может. Если даже самый базовый контракт API не выполняется, вы можете получить исключение: "Uncaught TypeError: prom.then не является функцией". Я думаю, что трюк здесь заключается в том, что авторы библиотек четко излагают свои потребности в API. a .then на поставляемом объекте. И тогда это зависит от того, кто строится поверх этого API, чтобы удостовериться, что этот метод доступен на объекте, в котором они проходят.

Я хотел бы также отметить здесь, что это также относится к Haskell, не так ли? Должен ли я быть настолько глупым, чтобы написать экземпляр для класса стилей, который все еще проверяет тип, не следуя его законам, я получу ошибки во время выполнения, не так ли?

Куда мы идем отсюда?

Продумав все это только сейчас, я думаю, что мы могли бы иметь преимущества принципа надежности даже в Haskell с гораздо меньшим (или даже нет (?)) риском для исключений/ошибок во время выполнения по сравнению с JavaScript: мы просто нужно, чтобы система типов была достаточно гранулярной, чтобы она могла отличить то, что мы хотим делать с данными, которыми мы управляем, и определить, все ли это безопасно или нет. Например. Гипотетический пример Text выше, я бы сделал ставку все еще в безопасности. И компилятор должен жаловаться только в том случае, если я пытаюсь использовать intersperse, и попрошу его квалифицировать его. Например. с T.intersperse, поэтому можно быть уверенным, какой из них я хочу использовать.

Как мы это делаем на практике? Нужна ли нам дополнительная поддержка, например. флаги расширения языка от GHC? Мы не можем.

Совсем недавно я нашел bookkeeper, который представляет собой реализацию анонимных записей, зависящую от типа времени компиляции.

Обратите внимание: следующее соображение с моей стороны, я не потратил много времени, чтобы попробовать экспериментировать с Bookkeeper. Но я намерен в своих проектах Haskell, чтобы увидеть, действительно ли то, что я пишу ниже, может быть достигнуто с помощью такого подхода.

С Bookkeeper я мог бы определить API так:

emptyBook & #then =: id & #fail =: const
  :: Bookkeeper.Internal.Book'
       '["fail" 'Data.Type.Map.:-> (a -> b -> a),
         "then" 'Data.Type.Map.:-> (a1 -> a1)]

Так как функции также являются первоклассными значениями. И в зависимости от того, какой API принимает этот Book в качестве аргумента, может быть очень конкретным то, что он требует от него: А именно функция #then и что она должна соответствовать определенной сигнатуре типа. И он не заботится ни о какой другой функции, которая может или не может присутствовать с какой-либо подписью. Все это проверено во время компиляции.

Prelude Bookkeeper
> let f o = (o ?: #foo) "a" "b" in f $ emptyBook & #foo =: (++)
"ab"

Заключение

Может быть, бухгалтера или что-то подобное окажутся полезными в моих экспериментах. Возможно, Рюкзак бросится на помощь с его общими определениями интерфейса. Или возникает другое решение. Но в любом случае, я надеюсь, мы сможем двигаться вперед, чтобы использовать принцип надежности. И это управление зависимостью Haskell также может "просто работать" большую часть времени и терпеть неудачу с ошибками типа только тогда, когда это действительно оправдано.

Означает ли это выше? Что-то непонятное? Отвечает ли он на ваш вопрос? Мне было бы интересно услышать.


Дальнейшее, возможно, соответствующее обсуждение можно найти в this/r/haskell reddit thread, где эта тема возникла совсем недавно, и Я думал отправить этот ответ в оба места.

Ответ 4

Вот вопрос, который в основном отвечает на одно и то же: fooobar.com/info/165852/...

Node.js не загрязняют глобальную область, поэтому, когда они требуются, они будут закрыты для модуля, который их требует, и это отличная функциональность.

Если для двух или более пакетов требуются разные версии одного и того же lib, NPM будет устанавливать их для каждого пакета, поэтому конфликтов никогда не произойдет.
Когда они этого не сделают, NPM установит только один раз этот lib.

С другой стороны, Bower, который является диспетчером пакетов для браузера, устанавливает только плоские зависимости, потому что libs будет перейдите в глобальную область, поэтому вы не можете установить jquery 1.xx и 2.xx Они будут экспортировать только те же jQuery и $ vars.

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

Учитывая, что большинство пакетов там еще не достигли версии 2.0, я считаю, что они сохранили тот же API в коммутаторе от v0.x.x до v1.0.0.