Callback / czy tak działa ? Jak z niego wyjść

1

Witam
Przechodząc z oop na FP walczę. Próbuję coś działać, ale sam nie wiem czy dobrze tworzę funkcje.
Chodzi mi o takie rozwiązanie. Wiem że stanów powinno się w FP unikać ale mniejsza z tym, może ktoś pomoże.

mamy funkcję które robią kolejne rzeczy :

  • 1 sprawdza czy stan 'running == true ' = > checkThatIsRunning
  • 2 jesli tak to pobiera z taba data - id i przekazuje do kolejnej = > getValuefromTab
  • 3 odbiera data.id z poprzedniej i konwertuje na czas przesyła dalej => convertForTime
  • 4 skonwertowany czas wrzuca w dom do elementu label = >

czy wywołanie funkcji poniżej może następować w taki sposób ? Event jest na parencie bo wykorzystuję go do toogle i pobrania data.id
No i tabów może być nieskończenie wiele.

I kolejne pytanie, co jeśli isRunning jest true, jak mam przerwać ten callback, mam wyłapywać do każdej funkcji drugi argument ?
Albo sprawdzać w każdej kolejnej funkcji czy argument jest nieprawidłowy aż dojdę do najwyższego elementu ?
Prosiłbym o jakąś sugestie. Chciałbym żeby w przypadku isRuning zwrócił np alert, ale nie wykonywał dalej callbacka tylko z tego wyszedł.
W przypadku kiedy isRunnig jest false robi dalej callback i przekazuje dane do elementu.

Znalazłem coś takiego :
https://medium.com/nmc-techblog/functional-error-handling-in-js-8b7f7e4fa092

Staram się coś deklaratywnie zacząć pisać, ale mózg stawia opór. Wiem że ifów powinienem unikać - podobnie statów

timerOptions.addEventListener("click", function (e) {
  setCountDownTimerLabel(
    convertForTime(getValuefromTab(checkThatIsRunning(e)))
  );
});
Kolejne funkcje:

const checkThatIsRunning = function (e) {
  if (state.pomodoro.isRunning) {
    return;
  }

const getValuefromTab = function (e) {
  const data = +e.target.closest(".timer__options-btn").dataset.time;
  if (!data) return;
  state.pomodoro.currentTime = data;
  return data;
};

const convertForTime = function (data) {
  const sec = data;
  let minutes = parseInt(sec / 60, 10);
  let seconds = parseInt(sec % 60, 10);
  minutes = minutes < 10 ? "0" + minutes : minutes;
  seconds = seconds < 10 ? "0" + seconds : seconds;
  const time = `${minutes}:${seconds}`;
  return time;
};

const setCountDownTimerLabel = function (data) {
  countDown.innerHTML = data;
  return data;
};
0

Przepraszam za spam, ale jak coś to nie chodzi mi o takie rozwiązanie:

const checkthat = function (e) {
  if (state.pomodoro.isRunning) {
    alert("cos nie tak");
    return;
  }
  setCountDownTimerLabel(convertForTime(getValuefromTab(e)));
};

Mógłbym sprawdzić w pierwszej funkcji np ten warunek i wywołać resztę, ale co jeśli będę chciał w środku jakieś funkcji coś sprawdzić.
Muszę to robić przez promise ? Czy jest inne rozwiązanie. Dla wielu pewnie trywialne jest to co piszę, ale właśnie takie proste rozkminy nie dają mi spać - żeby apka miała ręcę i nogi.

2

O ile dobrze rozumiem chcesz osiągnąć efekt by przepływ sterowania nie był zahardkodowany w pierszej funkcji (ani w kolejnych) - całkiem rozsądnie.
Jak ja bym to zrobił? Użyłbym HOF (higher order function) by sterować przepływem danych pomiędzy funkcjami. Najprostsze funkcje tego typu to pipe/compose, które spłaszczają zapis zagnieżdżonych wywołań funkcji, w stylu:

// Instead of:
d(c(b(a(arg))))

// you can do this:
pipe(a, b, c, d)(arg)

Takie funkcje mogą być dodatkowo wyposażone w logikę kontrolującą błędy (przerywającą łańcuch w przypadku błędu), rozwiązującą promisy itp.

Tu masz przykład zastosowania funkcji rail z pakietu @arrows, która kontroluje przepływ za pomocą rzucanych błędów:

const { rail } = require('@arrows/composition');

/* Mocking state for example purpose */

const state = {
  isRunning: false, // change to true to see error message
  currentTime: 0
};

const dataset = {
  time: 123 // change to undefined to see error message
};

const fakeOutput = {
  innerHTML: ''
};

/* Steps */

const checkThatIsRunning = () => {
  if (state.isRunning) {
    return new Error('TIMER_ALREADY_RUNNING');
  }

  return dataset;
};

const getValuefromTab = function(dataset) {
  const data = dataset.time;

  if (!data) {
    return new Error('NO_DATA');
  }

  state.currentTime = data;
  return data;
};

const convertForTime = function(data) {
  const sec = data;
  let minutes = parseInt(sec / 60, 10);
  let seconds = parseInt(sec % 60, 10);
  minutes = minutes < 10 ? '0' + minutes : minutes;
  seconds = seconds < 10 ? '0' + seconds : seconds;
  const time = `${minutes}:${seconds}`;
  return time;
};

const setCountDownTimerLabel = function(data) {
  fakeOutput.innerHTML = data;
  return data;
};

/* Composition */

const updateTimer = rail(
  checkThatIsRunning,
  getValuefromTab,
  convertForTime,
  setCountDownTimerLabel
);

/* Execution */

// For simplicity I will skip composing error handling and check it directly,
// but nothing stops you to build a proper pipeline for this:

const result = updateTimer();

const handleErrors = error => {
  switch (error.message) {
    case 'TIMER_ALREADY_RUNNING':
      console.log('Already running!');
      break;
    case 'NO_DATA':
      console.log('No data!');
      break;
  }
};

if (result instanceof Error) {
  handleErrors(result);
} else {
  console.log(`Result: ${result} | Label: ${fakeOutput.innerHTML}`);
}

Live demo: stackblitz (wklep node index.js we wbudowanej konsoli by odpalić program)

Inspiracja dla powstania biblioteki: https://vimeo.com/113707214

Oczywiście to tylko jeden ze sposobów rozwiązania problemu.

0

@Maciej Cąderek: Kosmos :) Zastanawiam się jak miałbym na to wpaść, szukałem informacji po zapytaniach googla, znalazłem info o promisach. Rails / torowiska gdzieś mi się obiły o uszy, z logicznego punktu widzenia właśnie te torowiska z przesównymi torami dają taką opcję wychwytywania błędów.

Mam jeszcze jedno pytanie:
Jak to wygląda np w firmie w pracy - można używać od tak np @arrows/composition z npm. Czy założenia projektu mogłyby tego zastrzec - czy to indywidualne.

I jeszcze jedno pytanie. Mógłbym to zrobić na żywca, ale czy takie rozwiązania się stosuje ? Wiadomo że chodzi o utrzymanie kodu i czytelność. Promisem bym mógł to zrobić, ale czym jeszcze ?

To co wysłałeś jest dla mnie mega spoko, tylko czy stosować to teraz wszędzie gdzie natrafię na taki problem.

0

@guzdziac55:

Zastanawiam się jak miałbym na to wpaść, szukałem informacji po zapytaniach googla, znalazłem info o promisach. Rails / torowiska gdzieś mi się obiły o uszy, z logicznego punktu widzenia właśnie te torowiska z przesównymi torami dają taką opcję wychwytywania błędów.

Jak to wygląda np w firmie w pracy - można używać od tak np @arrows/composition z npm. Czy założenia projektu mogłyby tego zastrzec - czy to indywidualne.

Jak ze wszystkim - to zależy. Rzadko klient ma jakieś specjalne wymagania (no chyba, że trafisz na jakąś specyficzną branżę jak banki itp), częściej to od zespołu zależy czy i jakie biblioteki są używane w projekcie. No i żeby FP miało ręce i nogi to cały zespół powinien rozumieć i chcieć pisać w funkcyjnym stylu. Kolejną rzeczą są frameworki typu React/Vue/Angular (czy np Express w przypadku Node'a), które mają często swoje rozwiązania dla podobnych problemów (choć tu też jest pewna elastyczność).

I jeszcze jedno pytanie. Mógłbym to zrobić na żywca, ale czy takie rozwiązania się stosuje ? Wiadomo że chodzi o utrzymanie kodu i czytelność.

Czy jest to popularne rozwiązanie? Raczej nie, ale nie ze względu, że jest to zła praktyka etc. (imo przeciwnie) - raczej po prostu mało ludzi się przejmuje / nie widzi zalet / nie jest zaznajomiona z paradygmatem.

Promisem bym mógł to zrobić, ale czym jeszcze?

Pakowanie takiego synchronicznego kodu w promisy tylko po to by sterować przepływem to imo zła praktyka. Może czegoś nie rozumiem - jak by to miało wyglądać?
Można by to zrobić też w blokach try-catch ale to też imo się bałagan robi szybko i nie jest zbyt przejrzyste.
Mainstreamowe rozwiązanie (z tych bardziej funkcyjnych) to virtual DOM i unidirectional data flow (np. React+Redux).
Kolejna funkcyjna opcja to streamy (np. ReactiveX, Cycle.js).
Ciekawą opcją są też CSP channels - choć to raczej niszowe (popularne w inych językach jak Clojure i Go) i nie znam dobrych bibliotek do tego.
Często można spotkać kombinacje wielu technik, w zależności od konkretnego zadania.

To co wysłałeś jest dla mnie mega spoko, tylko czy stosować to teraz wszędzie gdzie natrafię na taki problem.

A to od ciebe zależy (lub twojego zespołu) - jeśli dobrze pasuje to do danego problemu to czemu nie. Mogę być to trochę stronniczy bo ta akurat biblioteka jest mojego autorstwa, ale większość funkcji w pakiecie to moje wersje powszechnie stosowanych narzedzi funkcyjnych (compose/pipe/tap/curry) obecne w wielu innych bibliotekach i językach programowania. Takie funkcje możesz nawet sobie samemu napisać (nie są to jakieś bardzo skompikowane rzeczy), choć są one tak generyczne, że akurat użycie gotowej libki w tym wypadku ma sens.

Z punktu widzenia kariery to nabezpieczniejszą opcją jest chyba React i jego ekosystem (jeśli chodzi o frontend), który daje gotowe rozwiązania dla części problemów, ale jest na tyle elastyczny, że ogólna wiedza ne temat FP i (szczególności HOF i kompozycji) jest i tak bardzo przydatna by zapanować nad chaosem w projekcie :)

0

@Maciej Cąderek: dzięki za poświęcony czas na odpisanie.
Zainteresowałem się FP, bo wcześniej pisałem coś tam w javie - zobaczyłem w es6 klasy i magicznie nagle zacząłem pisać szybciej. Później zauważyłem że te klasy to tak nie całkiem klasy. Dostałem informacje że warto znać, ale FP jest bardziej popularny.

Mam pytanie do osoby ktora lata spędziła z kodem. Wiadomo człowiek uczy się cały czas. Ale twoim zdaniem, z perspektywy doświadczonego usera. Lepiej znając podstawy js szukać inspiracji i stabilizacji w frameworku np Reakcie. Chodzi mi o kwestię wystartowania w branży, zaciągnięcia się na staż - cokolwiek. Czy rzeczy poruszone jak tutaj typu rails też powinienem znać - pisałeś że frameworki mają gotowe rozwiązania - wiadomo że lepiej wiedziec jak cos dziala pod spodem.

No i żeby FP miało ręce i nogi to cały zespół powinien rozumieć i chcieć pisać w funkcyjnym stylu

Rzadko spotykane w zespołach ? Twoim zdaniem warto trzymać się i rozumieć koncepcję FP ?

Sory za pytania, piszę do szuflady, mam kilka szkieletów programów w samym jsie, zarówno pisanych na pałę jak i na klasach. Często dochodząc już do momenu gdzize funkcjonalnosci jest sporo mam zagwozdkę że to co napisałem jest po prostu obrzydliwe. Tzn wiem że mogłem to zrobić lepiej, ale nie znam czegoś czego powinienem. Dziwnie to brzmi, ale tak jest.

1

Warto. Kod możesz pisać nawet wyłącznie na klasach, a i tak stosować idee FP. Po prostu wtedy wychodzi lepszy kod. Więc to czy zespół to stosuje czy nie, dalej znajomość FP się przydaje.

Ale z drugiej strony też nie ma sensu za bardzo się fiksować, bo możesz się zblokować próbując zbudować jakiś idealny kod. Po prostu pisz i z czasem sam zaczniesz zauważać, jakie konstrukcje działają dobrze, a jakie mogą ugryźć w dupę w przyszłości.

A, i w sumie najlepszy sposób, żeby się nauczyć to popisać w jakimś Haskellu czy innym języku nakierowanym na FP. Bo tam po prostu inaczej kodu nie napiszesz, więc odpada zastanawianie się czy to jest FP czy nie FP.

1 użytkowników online, w tym zalogowanych: 0, gości: 1