Очистка интервала в Reason

В Разуме, какой был бы самый элегантный способ иметь интервал, который очищается сам по себе, когда выполняется какое-то условие? В JavaScript я мог бы сделать:

var myInterval = setInterval(function () {
    // do some stuff
    if (fancyCondition) {
        clearInterval(myInterval);
    }
}, 1000);

В Reason лучшее, что я придумал до сих пор, это:

let intervalIdRef = ref(None);
let clearInterval = () =>
    switch (intervalIdRef^) {
    | Some(intervalId) => Js.Global.clearInterval(intervalId)
    | None => ()
    };
let intervalId = Js.Global.setInterval(() => {
    /* do some stuff */
    fancyCondition ? clearInterval() : ();
}, 1000);
intervalIdRef := Some(intervalId);

Есть ли способ избежать использования ref?


person noziar    schedule 16.05.2018    source источник


Ответы (1)


setInterval/clearInterval по своей природе изменчивы, но даже если бы это был не ваш fancyCondition, он все равно был бы, поэтому удаление ref здесь не принесет вам многого. Я думаю, что даже с ref его можно было бы улучшить за счет инкапсуляции, и в зависимости от вашего fancyCondition мы должны иметь возможность получить такое же поведение чисто функциональным способом, используя setTimeout вместо setInterval/clearInterval.

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

let intervalIdRef = ref(None);
let count = ref(0);

let clearInterval = () =>
  switch (intervalIdRef^) {
    | Some(intervalId) => Js.Global.clearInterval(intervalId)
    | None => ()
  };

let intervalId = Js.Global.setInterval(() => {
  if (count^ < 5) {
    Js.log2("tick", count^);
    count := count^ + 1;
  } else {
    Js.log("abort!");
    clearInterval();
  }
}, 200);

intervalIdRef := Some(intervalId);

Первое, что, я думаю, мы должны сделать, это инкапсулировать состояние/дескриптор таймера, обернув его в функцию и передать clearInterval обратному вызову вместо того, чтобы использовать его как отдельную функцию, которую мы могли бы вызывать несколько раз, не зная, действительно ли она что-то делает:

let setInterval = (timeout, action) => {
  let intervalIdRef = ref(None);
  let clear = () =>
    switch (intervalIdRef^) {
      | Some(intervalId) => Js.Global.clearInterval(intervalId)
      | None => ()
    };

  let intervalId = Js.Global.setInterval(() => action(~clear), timeout);
  intervalIdRef := Some(intervalId);
};

let count = ref(0);
setInterval(200, (~clear) => {
  if (count^ < 5) {
    Js.log2("tick", count^);
    count := count^ + 1;
  } else {
    Js.log("abort!");
    clear();
  }
});

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

let rec setTimeout = (timeout, action, state) => {
  let continue = setTimeout(timeout, action);
  let _:Js.Global.timeoutId =
    Js.Global.setTimeout(() => action(~continue, state), timeout)
};

setTimeout(200, (~continue, count) => {
  if (count < 5) {
    Js.log2("tick", count);
    continue(count + 1);
  } else {
    Js.log("abort!");
  }
}, 0);

Здесь мы немного перевернули проблему с ног на голову. Вместо того, чтобы использовать setInterval и clearInterval и передавать функцию clear в наш обратный вызов, мы передаем ему функцию continue, которая вызывается, когда мы хотим продолжить, а не когда мы хотим выручить. Это позволяет нам передавать состояние вперед и изменять состояние без использования мутации и refs, используя вместо этого рекурсию. И делает это с меньшим количеством кода. Я думаю, что это было бы довольно элегантно, если бы не то, о чем вы просили :)

person glennsl    schedule 19.05.2018