stream()
Opis
Strumień to reaktywna struktura danych, działająca podobnie do komórek w arkuszach kalkulacyjnych.
Na przykład, w arkuszu kalkulacyjnym, jeśli A1 = B1 + C1
, to zmiana wartości B1
lub C1
automatycznie zmieni wartość A1
.
Podobnie, możesz zdefiniować strumień, który zależy od innych strumieni, tak aby zmiana wartości jednego automatycznie aktualizowała drugi. Jest to przydatne, gdy masz kosztowne obliczenia i chcesz je uruchamiać tylko wtedy, gdy jest to konieczne, zamiast przy każdym odświeżeniu (redraw).
Strumienie nie są częścią podstawowej dystrybucji Mithril.js. Aby dołączyć moduł Streams, użyj:
var Stream = require('mithril/stream');
Możesz również pobrać moduł bezpośrednio, jeśli twoje środowisko nie obsługuje narzędzi do bundlingu:
<script src="https://unpkg.com/mithril/stream/stream.js"></script>
Po załadowaniu bezpośrednio za pomocą tagu <script>
(zamiast require
), biblioteka strumieni będzie dostępna jako window.m.stream
. Jeśli window.m
jest już zdefiniowane (np. dlatego, że używasz również głównego skryptu Mithril.js), dołączy się do istniejącego obiektu. W przeciwnym razie tworzy nowy window.m
. Jeśli chcesz używać strumieni w połączeniu z Mithril.js jako surowe tagi skryptów, powinieneś dołączyć Mithril.js do swojej strony przed mithril/stream
, ponieważ mithril
w przeciwnym razie nadpisze obiekt window.m
zdefiniowany przez mithril/stream
. Nie stanowi to problemu, gdy biblioteki są używane jako moduły CommonJS (za pomocą require(...)
).
Sygnatura
Tworzy nowy strumień.
stream = Stream(value)
Argument | Typ | Wymagany | Opis |
---|---|---|---|
value | any | Nie | Jeśli ten argument jest obecny, wartość strumienia jest ustawiana na niego. |
zwraca | Stream | Zwraca strumień. |
Statyczne elementy członkowskie
Stream.combine
Tworzy strumień obliczeniowy, który aktualizuje się reaktywnie, jeśli którykolwiek z jego strumieni wejściowych zostanie zaktualizowany. Zobacz łączenie strumieni.
stream = Stream.combine(combiner, streams)
Argument | Typ | Wymagany | Opis |
---|---|---|---|
combiner | (Stream..., Array) -> any | Tak | Szczegóły w opisie argumentu combiner. |
streams | Array<Stream> | Tak | Lista strumieni do połączenia. |
zwraca | Stream | Zwraca strumień. |
combiner
Określa, w jaki sposób generowana jest wartość strumienia obliczeniowego. Zobacz łączenie strumieni.
any = combiner(streams..., changed)
Argument | Typ | Wymagany | Opis |
---|---|---|---|
streams... | splat of Streams | Nie | Splat strumieni, które odpowiadają strumieniom przekazanym jako drugi argument do stream.combine . |
changed | Array<Stream> | Tak | Lista strumieni, które spowodowały aktualizację. |
zwraca | any | Zwraca obliczoną wartość. |
Stream.merge
Tworzy strumień, którego wartością jest tablica wartości ze strumieni wejściowych.
stream = Stream.merge(streams)
Argument | Typ | Wymagany | Opis |
---|---|---|---|
streams | Array<Stream> | Tak | Lista strumieni. |
zwraca | Stream | Zwraca strumień zawierający tablicę wartości ze strumieni wejściowych. |
Stream.scan
Tworzy nowy strumień z wynikami wywołania funkcji na każdej wartości w strumieniu, z akumulatorem i wartością przychodzącą.
Zauważ, że możesz zapobiec aktualizacji zależnych strumieni, zwracając specjalną wartość Stream.SKIP
wewnątrz funkcji akumulatora.
stream = Stream.scan(fn, accumulator, stream)
Argument | Typ | Wymagany | Opis |
---|---|---|---|
fn | (accumulator, value) -> result | SKIP | Tak | Funkcja, która przyjmuje akumulator i wartość, a następnie zwraca nową wartość akumulatora (tego samego typu). |
accumulator | any | Tak | Wartość początkowa dla akumulatora. |
stream | Stream | Tak | Strumień zawierający wartości. |
zwraca | Stream | Zwraca nowy strumień zawierający wynik. |
Stream.scanMerge
Pobiera tablicę par strumieni i funkcji skanowania i łączy wszystkie te strumienie za pomocą podanych funkcji w jeden strumień.
stream = Stream.scanMerge(pairs, accumulator)
Argument | Typ | Wymagany | Opis |
---|---|---|---|
pairs | Array<[Stream, (accumulator, value) -> value]> | Tak | Tablica par: strumień i funkcja skanowania. |
accumulator | any | Tak | Wartość początkowa dla akumulatora. |
zwraca | Stream | Zwraca nowy strumień zawierający wynik. |
Stream.lift
Tworzy strumień obliczeniowy, który aktualizuje się reaktywnie, jeśli którykolwiek z jego strumieni wejściowych zostanie zaktualizowany. Zobacz łączenie strumieni. W przeciwieństwie do combine
, strumienie wejściowe są przekazywane jako zmienna liczba argumentów (zamiast tablicy), a funkcja zwrotna (callback) otrzymuje wartości strumieni zamiast strumieni. Nie ma parametru changed
. Jest to ogólnie bardziej przyjazna dla użytkownika funkcja niż combine
.
stream = Stream.lift(lifter, stream1, stream2, ...)
Argument | Typ | Wymagany | Opis |
---|---|---|---|
lifter | (any...) -> any | Tak | Zobacz argument lifter. |
streams... | list of Streams | Tak | Strumienie, które zostaną przetworzone przez funkcję lifter . |
zwraca | Stream | Zwraca strumień. |
lifter
Określa, w jaki sposób generowana jest wartość strumienia obliczeniowego. Zobacz łączenie strumieni.
any = lifter(streams...)
Argument | Typ | Wymagany | Opis |
---|---|---|---|
streams... | splat of Streams | Nie | Splat strumieni, które odpowiadają wartościom strumieni przekazanych do stream.lift . |
zwraca | any | Zwraca obliczoną wartość. |
Stream.SKIP
Specjalna wartość (SKIP), która może być zwracana w callbackach strumienia, aby pominąć wykonanie zależnych strumieni.
Stream["fantasy-land/of"]
Ta metoda jest funkcjonalnie identyczna z stream
. Istnieje, aby być zgodną ze specyfikacją Applicative Fantasy Land. Więcej informacji można znaleźć w sekcji Czym jest Fantasy Land.
stream = Stream["fantasy-land/of"](value)
Argument | Typ | Wymagany | Opis |
---|---|---|---|
value | any | Nie | Jeśli ten argument jest obecny, wartość strumienia jest ustawiana na niego. |
zwraca | Stream | Zwraca strumień. |
Elementy członkowskie instancji
stream.map
Tworzy zależny strumień, którego wartość jest ustawiana na wynik callbacku. Ta metoda jest aliasem stream["fantasy-land/map"].
dependentStream = stream().map(callback)
Argument | Typ | Wymagany | Opis |
---|---|---|---|
callback | any -> any | Tak | Funkcja, której wynik staje się wartością strumienia. |
zwraca | Stream | Zwraca strumień. |
stream.end
Strumień, który wyrejestrowuje zależne strumienie, gdy jest ustawiony na true
. Zobacz stan zakończony.
endStream = stream().end
stream["fantasy-land/of"]
Ta metoda jest funkcjonalnie identyczna z stream
. Istnieje, aby być zgodną ze specyfikacją Applicative Fantasy Land. Więcej informacji można znaleźć w sekcji Czym jest Fantasy Land.
stream = stream()["fantasy-land/of"](value)
Argument | Typ | Wymagany | Opis |
---|---|---|---|
value | any | Nie | Jeśli ten argument jest obecny, wartość strumienia jest ustawiana na niego. |
zwraca | Stream | Zwraca strumień. |
stream["fantasy-land/map"]
Tworzy zależny strumień, którego wartość jest ustawiana na wynik callbacku. Zobacz łańcuchowe łączenie strumieni.
Ta metoda istnieje, aby być zgodną ze specyfikacją Applicative Fantasy Land. Więcej informacji można znaleźć w sekcji Czym jest Fantasy Land.
dependentStream = stream()["fantasy-land/map"](callback)
Argument | Typ | Wymagany | Opis |
---|---|---|---|
callback | any -> any | Tak | Funkcja, której wynik staje się wartością strumienia. |
zwraca | Stream | Zwraca strumień. |
stream["fantasy-land/ap"]
Nazwa tej metody oznacza apply
(zastosuj). Jeśli strumień a
ma funkcję jako swoją wartość, inny strumień b
może użyć go jako argumentu do b.ap(a)
. Wywołanie ap
wywoła funkcję z wartością strumienia b
jako argumentem i zwróci inny strumień, którego wartością jest wynik wywołania funkcji. Ta metoda istnieje, aby być zgodną ze specyfikacją Applicative Fantasy Land. Więcej informacji można znaleźć w sekcji Czym jest Fantasy Land.
stream = stream()["fantasy-land/ap"](apply)
Argument | Typ | Wymagany | Opis |
---|---|---|---|
apply | Stream | Tak | Strumień, którego wartością jest funkcja. |
zwraca | Stream | Zwraca strumień. |
Podstawowe użycie
Strumienie nie są częścią podstawowej dystrybucji Mithril.js. Aby dołączyć je do projektu, zaimportuj (require) jego moduł:
var stream = require('mithril/stream');
Strumienie jako zmienne
stream()
zwraca strumień. Na najbardziej podstawowym poziomie, strumień działa podobnie do zmiennej lub właściwości getter-setter: może przechowywać stan, który można modyfikować.
var username = stream('John');
console.log(username()); // loguje "John"
username('John Doe');
console.log(username()); // loguje "John Doe"
Główna różnica polega na tym, że strumień jest funkcją, a zatem może być używany w funkcjach wyższego rzędu.
var users = stream();
// pobierz użytkowników z serwera za pomocą API fetch
fetch('/api/users')
.then(function (response) {
return response.json();
})
.then(users);
W powyższym przykładzie, strumień users
jest wypełniany danymi odpowiedzi, gdy żądanie zostanie rozwiązane (resolve).
Powiązania dwukierunkowe
Dwukierunkowe powiązanie ze strumieniem może być również realizowane z użyciem callbacków zdarzeń.
// strumień
var user = stream('');
// powiązanie dwukierunkowe ze strumieniem
m('input', {
oninput: function (e) {
user(e.target.value);
},
value: user(),
});
W powyższym przykładzie, gdy użytkownik wpisuje tekst w polu input, strumień user
jest aktualizowany do wartości pola input.
Właściwości obliczeniowe
Strumienie są przydatne do implementowania właściwości obliczeniowych:
var title = stream('');
var slug = title.map(function (value) {
return value.toLowerCase().replace(/\W/g, '-');
});
title('Hello world');
console.log(slug()); // loguje "hello-world"
W powyższym przykładzie, wartość slug
jest obliczana, gdy title
jest aktualizowany, a nie gdy slug
jest odczytywany.
Oczywiście możliwe jest również obliczanie właściwości na podstawie wielu strumieni:
var firstName = stream('John');
var lastName = stream('Doe');
var fullName = stream.merge([firstName, lastName]).map(function (values) {
return values.join(' ');
});
console.log(fullName()); // loguje "John Doe"
firstName('Mary');
console.log(fullName()); // loguje "Mary Doe"
Mithril.js aktualizuje właściwości obliczeniowe atomowo. Oznacza to, że strumienie zależne od wielu innych strumieni zostaną wywołane tylko raz na aktualizację wartości, niezależnie od złożoności grafu zależności.
Łączenie strumieni
Strumienie można łączyć za pomocą metody map
. Strumień utworzony za pomocą map
nazywany jest również strumieniem zależnym.
// strumień nadrzędny
var value = stream(1);
// strumień zależny
var doubled = value.map(function (value) {
return value * 2;
});
console.log(doubled()); // loguje 2
Strumienie zależne są reaktywne: ich wartości są aktualizowane za każdym razem, gdy wartość ich strumienia nadrzędnego jest aktualizowana. Dzieje się tak niezależnie od tego, czy strumień zależny został utworzony przed, czy po ustawieniu wartości strumienia nadrzędnego.
Możesz zapobiec aktualizacji zależnych strumieni, zwracając specjalną wartość Stream.SKIP
.
var skipped = stream(1).map(function (value) {
return stream.SKIP;
});
skipped.map(function () {
// nigdy się nie uruchomi
});
Kombinowanie strumieni
Strumienie mogą zależeć od więcej niż jednego strumienia nadrzędnego. Tego rodzaju strumienie można tworzyć za pomocą stream.merge()
var a = stream('hello');
var b = stream('world');
var greeting = stream.merge([a, b]).map(function (values) {
return values.join(' ');
});
console.log(greeting()); // loguje "hello world"
Lub możesz użyć funkcji pomocniczej stream.lift()
var a = stream('hello');
var b = stream('world');
var greeting = stream.lift(
function (_a, _b) {
return _a + ' ' + _b;
},
a,
b
);
console.log(greeting()); // loguje "hello world"
Metoda stream.combine()
to funkcja niższego poziomu, która udostępnia strumienie bezpośrednio w obliczeniach reaktywnych, co pozwala na bardziej zaawansowane zastosowania.
var a = stream(5);
var b = stream(7);
var added = stream.combine(
function (a, b) {
return a() + b();
},
[a, b]
);
console.log(added()); // loguje 12
Strumień może zależeć od dowolnej liczby strumieni i gwarantuje się, że będzie aktualizowany atomowo. Na przykład, jeśli strumień A ma dwa zależne strumienie B i C, a czwarty strumień D jest zależny zarówno od B, jak i C, strumień D zaktualizuje się tylko raz, jeśli wartość A się zmieni. Gwarantuje to, że callback dla strumienia D nigdy nie zostanie wywołany z niestabilnymi wartościami, takimi jak wtedy, gdy B ma nową wartość, ale C ma starą wartość. Atomowość przynosi również korzyści wydajnościowe w postaci braku niepotrzebnego ponownego obliczania zależnych strumieni.
Możesz zapobiec aktualizacji zależnych strumieni, zwracając specjalną wartość Stream.SKIP
.
var skipped = stream.combine(
function (stream) {
return stream.SKIP;
},
[stream(1)]
);
skipped.map(function () {
// nigdy się nie uruchomi
});
Stany strumienia
Strumień może znajdować się w jednym z trzech stanów: oczekującym (pending), aktywnym (active) lub zakończonym (ended).
Stan oczekujący
Strumienie w stanie oczekującym można utworzyć, wywołując stream()
bez parametrów.
var pending = stream();
Jeśli strumień jest zależny od więcej niż jednego strumienia, a którykolwiek z jego strumieni nadrzędnych jest w stanie oczekującym, strumień zależny jest również w stanie oczekującym i nie aktualizuje swojej wartości.
var a = stream(5);
var b = stream(); // strumień w stanie oczekującym
var added = stream.combine(
function (a, b) {
return a() + b();
},
[a, b]
);
console.log(added()); // loguje undefined
W powyższym przykładzie, added
jest strumieniem w stanie oczekującym, ponieważ jego strumień nadrzędny b
jest również w stanie oczekującym.
Dotyczy to również strumieni zależnych utworzonych za pomocą stream.map
:
var value = stream();
var doubled = value.map(function (value) {
return value * 2;
});
console.log(doubled()); // loguje undefined, ponieważ `doubled` jest w stanie oczekującym
Stan aktywny
Gdy strumień otrzyma wartość, staje się aktywny (chyba że strumień jest zakończony).
var stream1 = stream('hello'); // stream1 jest aktywny
var stream2 = stream(); // stream2 zaczyna w stanie oczekującym
stream2('world'); // a następnie staje się aktywny
Strumień zależny z wieloma strumieniami nadrzędnymi staje się aktywny, jeśli wszystkie jego strumienie nadrzędne są aktywne.
var a = stream('hello');
var b = stream();
var greeting = stream.merge([a, b]).map(function (values) {
return values.join(' ');
});
W powyższym przykładzie, strumień a
jest aktywny, ale b
jest w stanie oczekującym. Ustawienie b("world")
spowodowałoby, że b
stałby się aktywny, a zatem greeting
również stałby się aktywny i zostałby zaktualizowany do wartości "hello world"
.
Stan zakończony
Strumień może przestać wpływać na swoje zależne strumienie, wywołując stream.end(true)
. To skutecznie usuwa połączenie między strumieniem a jego zależnymi strumieniami.
var value = stream();
var doubled = value.map(function (value) {
return value * 2;
});
value.end(true); // ustaw stan zakończony
value(5);
console.log(doubled());
// loguje undefined, ponieważ `doubled` nie zależy już od `value`
Strumienie w stanie zakończonym nadal mają semantykę kontenera stanu (state container), tj. nadal można ich używać jako getter-setterów, nawet po ich zakończeniu.
var value = stream(1);
value.end(true); // ustaw stan zakończony
console.log(value(1)); // loguje 1
value(2);
console.log(value()); // loguje 2
Zakończenie strumienia może być przydatne w przypadkach, gdy strumień ma ograniczoną żywotność (na przykład, reagowanie na zdarzenia mousemove
tylko wtedy, gdy element DOM jest przeciągany, ale nie po jego upuszczeniu).
Serializacja strumieni
Strumienie implementują metodę .toJSON()
. Gdy strumień jest przekazywany jako argument do funkcji JSON.stringify()
, wartość strumienia jest serializowana.
var value = stream(123);
var serialized = JSON.stringify(value);
console.log(serialized); // loguje 123
Strumienie nie wywołują renderowania
W przeciwieństwie do bibliotek takich jak Knockout, strumienie Mithril.js nie wywołują ponownego renderowania szablonów. Odświeżanie (redrawing) odbywa się w odpowiedzi na obsługę zdarzeń zdefiniowaną w widokach komponentów Mithril.js, zmiany trasy lub po rozwiązaniu wywołań m.request
.
Jeśli odświeżanie jest pożądane w odpowiedzi na inne zdarzenia asynchroniczne (np. setTimeout
/setInterval
, subskrypcja websocket, obsługa zdarzeń biblioteki zewnętrznej), należy ręcznie wywołać m.redraw()
.
Czym jest Fantasy Land
Fantasy Land określa interoperacyjność wspólnych struktur algebraicznych. Prościej mówiąc, biblioteki zgodne ze specyfikacją Fantasy Land pozwalają na pisanie uniwersalnego kodu funkcyjnego, niezależnie od wewnętrznej implementacji poszczególnych bibliotek.
Na przykład, powiedzmy, że chcemy utworzyć ogólną funkcję o nazwie plusOne
. Naiwna implementacja wyglądałaby tak:
function plusOne(a) {
return a + 1;
}
Problem z tą implementacją polega na tym, że można jej używać tylko z liczbą. Jednak możliwe jest, że logika generująca wartość dla a
może również generować stan błędu (opakowany w Maybe lub Either z biblioteki takiej jak Sanctuary lub Ramda-Fantasy), lub może to być strumień Mithril.js, strumień Flyd itp. Idealnie, nie chcielibyśmy pisać podobnej wersji tej samej funkcji dla każdego możliwego typu, jaki a
może mieć i nie chcielibyśmy pisać kodu opakowującego/rozpakowującego/obsługi błędów wielokrotnie.
W tym miejscu Fantasy Land może pomóc. Przepiszmy tę funkcję w kategoriach algebry Fantasy Land:
var fl = require('fantasy-land');
function plusOne(a) {
return a[fl.map](function (value) {
return value + 1;
});
}
Teraz ta metoda działa z dowolnym zgodnym z Fantasy Land Funktorem, takim jak R.Maybe
, S.Either
, stream
itp.
Ten przykład może wydawać się zawiły, ale jest to kompromis w złożoności: naiwna implementacja plusOne
ma sens, jeśli masz prosty system i tylko zwiększasz liczby, ale implementacja Fantasy Land staje się potężniejsza, jeśli masz duży system z wieloma abstrakcjami opakowującymi i algorytmami wielokrotnego użytku.
Decydując, czy powinieneś przyjąć Fantasy Land, powinieneś wziąć pod uwagę znajomość programowania funkcyjnego przez twój zespół i być realistą co do poziomu dyscypliny, jaki twój zespół może zobowiązać się do utrzymania jakości kodu (w porównaniu z presją pisania nowych funkcji i dotrzymywania terminów). Programowanie w stylu funkcyjnym w dużym stopniu zależy od kompilowania, kuratorowania i opanowywania dużego zestawu małych, precyzyjnie zdefiniowanych funkcji, a zatem nie jest odpowiednie dla zespołów, które nie mają solidnych praktyk dokumentacyjnych i/lub brakuje im doświadczenia w językach zorientowanych funkcyjnie.