Как бороться с побочными эффектами в коде встряхивания дерева?

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

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

let audio = new Audio(); // or document.createElement('audio')
let canPlay = {
  ogg: audio.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, '');
  mp3: audio.canPlayType('audio/mpeg; codecs="mp3"').replace(/^no$/, '');
  // ...
};

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

Ответ 1

Вы можете удалить страницу из книги на Haskell/PureScript и просто ограничить себя от появления побочных эффектов при импорте модуля. Вместо этого вы экспортируете thunk, который представляет собой побочный эффект, например, получения доступа к глобальному элементу Audio в браузере пользователя, и параметризуете другие функции/значения по отношению к значению, которое создает этот thunk.

Вот как это будет выглядеть для вашего фрагмента кода:

// :: type IO a = () -!-> a

// :: IO Audio
let getAudio = () => new Audio();

// :: Audio -> { [MimeType]: Boolean }
let canPlay = audio => {
  ogg: audio.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, '');
  mp3: audio.canPlayType('audio/mpeg; codecs="mp3"').replace(/^no$/, '');
  // ...
};

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

Это довольно очевидно, как подключить все эти новые параметры вручную, но это может стать утомительным. Есть несколько методов, чтобы смягчить это; подход, который вы снова можете украсть из Haskell/PureScript, заключается в использовании монады считывателя, которая облегчает своего рода внедрение зависимостей для программ, состоящих из простых функций.

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

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

Ответ 2

Вы можете реализовать модуль, чтобы предоставить вам аналогичную схему использования, предложенную в вашем вопросе, с использованием audio() для доступа к аудиообъекту и canPlay без вызова функции. Это можно сделать, запустив конструктор Audio в функции, как предложил Асад, и затем вызывая эту функцию каждый раз, когда вы хотите получить к ней доступ. Для canPlay мы можем использовать Proxy, позволяющий выполнять индексацию массива в виде функции.

Предположим, мы создали файл audio.js:

let audio = () => new Audio();
let canPlay = new Proxy({}, {
    get: (target, name) => {
        switch(name) {
            case 'ogg':
                return audio().canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, '');
            case 'mp3':
                return audio().canPlayType('audio/mpeg; codecs="mp3"').replace(/^no$/, '');
        }
    }
});

export {audio, canPlay}

Вот результаты работы с различными файлами index.js, rollup index.js -f iife:

import {} from './audio';
(function () {
    'use strict';



}());
import {audio} from './audio';

console.log(audio());
(function () {
    'use strict';

    let audio = () => new Audio();

    console.log(audio());

}());
import {canPlay} from './audio';

console.log(canPlay['ogg']);
(function () {
    'use strict';

    let audio = () => new Audio();
    let canPlay = new Proxy({}, {
        get: (target, name) => {
            switch(name) {
                case 'ogg':
                    return audio().canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/, '');
                case 'mp3':
                    return audio().canPlayType('audio/mpeg; codecs="mp3"').replace(/^no$/, '');
            }
        }
    });

    console.log(canPlay['ogg']);

}());

Кроме того, нет способа реализовать audio как первоначально предполагалось, если вы хотите сохранить свойства, описанные в вопросе. Другими короткими возможностями audio() являются +audio или audio'' (как показано здесь: вызов функции без круглых скобок), что можно считать более запутанным.

Наконец, другие глобальные переменные, которые не включают индекс массива или вызов функции, должны быть реализованы аналогичным образом, чтобы let audio =() => new Audio(); ,