stream()
Description
Un flux de données (stream) est une structure de données réactive, similaire aux cellules dans les applications de tableur.
Par exemple, dans un tableur, si A1 = B1 + C1
, alors modifier la valeur de B1
ou C1
change automatiquement la valeur de A1
.
De même, vous pouvez faire dépendre un stream d'autres streams de sorte que modifier la valeur de l'un mette automatiquement à jour l'autre. Ceci est utile pour les calculs coûteux que vous souhaitez n'exécuter qu'en cas de besoin, plutôt qu'à chaque redessin par exemple.
Les streams ne sont pas inclus dans la distribution principale de Mithril.js. Pour inclure le module stream, utilisez :
var Stream = require('mithril/stream');
Vous pouvez également télécharger le module directement si votre environnement ne prend pas en charge une chaîne d'outils d'assemblage (bundling) :
<script src="https://unpkg.com/mithril/stream/stream.js"></script>
Lorsqu'il est chargé directement avec une balise <script>
(plutôt que requis), le module stream sera exposé sous window.m.stream
. Si window.m
est déjà défini (par exemple, parce que vous utilisez également le script principal de Mithril.js), il s'attachera à l'objet existant. Sinon, il crée un nouvel objet window.m
. Si vous voulez utiliser les streams en conjonction avec Mithril.js via des balises <script>
, vous devriez inclure Mithril.js dans votre page avant mithril/stream
, car mithril
écrasera sinon l'objet window.m
défini par mithril/stream
. Ce n'est pas un problème lorsque les modules sont consommés via CommonJS (en utilisant require(...)
).
Signature
Crée un stream.
stream = Stream(value)
Argument | Type | Obligatoire | Description |
---|---|---|---|
value | any | Non | Si cet argument est présent, la valeur du stream lui est affectée. |
returns | Stream | Retourne un stream. |
Membres statiques
Stream.combine
Crée un stream calculé qui se met à jour réactivement si l'un de ses streams amont est mis à jour. Voir la section Combiner des streams.
stream = Stream.combine(combiner, streams)
Argument | Type | Obligatoire | Description |
---|---|---|---|
combiner | (Stream..., Array) -> any | Oui | Voir l'argument combiner. |
streams | Array<Stream> | Oui | Une liste de streams à combiner. |
returns | Stream | Retourne un stream. |
combiner
Spécifie comment la valeur d'un stream calculé est générée. Voir la section Combiner des streams.
any = combiner(streams..., changed)
Argument | Type | Obligatoire | Description |
---|---|---|---|
streams... | liste de Streams | Non | Liste de zéro ou plusieurs streams qui correspondent aux streams passés comme second argument à stream.combine . |
changed | Array<Stream> | Oui | Liste des streams qui ont été affectés par une mise à jour. |
returns | any | Retourne une valeur calculée. |
Stream.merge
Crée un stream dont la valeur est un tableau contenant les valeurs de chaque stream d'un tableau de streams.
stream = Stream.merge(streams)
Argument | Type | Obligatoire | Description |
---|---|---|---|
streams | Array<Stream> | Oui | Une liste de streams. |
returns | Stream | Retourne un stream dont la valeur est un tableau des valeurs des streams d'entrée. |
Stream.scan
Crée un nouveau stream avec les résultats de l'appel de la fonction sur chaque valeur du stream, en utilisant un accumulateur et la valeur entrante.
Notez que vous pouvez empêcher la mise à jour des streams dépendants en retournant la valeur spéciale stream.SKIP
à l'intérieur de la fonction d'accumulateur.
stream = Stream.scan(fn, accumulator, stream)
Argument | Type | Obligatoire | Description |
---|---|---|---|
fn | (accumulator, value) -> result | SKIP | Oui | Une fonction qui prend un accumulateur et une valeur, et retourne une nouvelle valeur d'accumulateur du même type. |
accumulator | any | Oui | La valeur de départ pour l'accumulateur. |
stream | Stream | Oui | Stream contenant les valeurs. |
returns | Stream | Retourne un nouveau stream contenant le résultat. |
Stream.scanMerge
Prend un tableau de paires de streams et de fonctions de scan, et fusionne tous ces streams en utilisant les fonctions données dans un seul stream.
stream = Stream.scanMerge(pairs, accumulator)
Argument | Type | Obligatoire | Description |
---|---|---|---|
pairs | Array<[Stream, (accumulator, value) -> value]> | Oui | Un tableau de tuples de stream et de fonctions de scan. |
accumulator | any | Oui | La valeur de départ pour l'accumulateur. |
returns | Stream | Retourne un nouveau stream contenant le résultat. |
Stream.lift
Crée un stream calculé qui se met à jour réactivement si l'un de ses streams amont est mis à jour. Voir la section Combiner des streams. Contrairement à combine
, les streams d'entrée sont passés comme un nombre variable d'arguments (au lieu d'un tableau) et le callback reçoit les valeurs des streams au lieu des streams eux-mêmes. Il n'y a pas de paramètre changed
. C'est généralement une fonction plus conviviale que combine
.
stream = Stream.lift(lifter, stream1, stream2, ...)
Argument | Type | Obligatoire | Description |
---|---|---|---|
lifter | (any...) -> any | Oui | Voir l'argument lifter. |
streams... | liste de Streams | Oui | Streams à "lifter". |
returns | Stream | Retourne un stream. |
lifter
Spécifie comment la valeur d'un stream calculé est générée. Voir la section Combiner des streams.
any = lifter(streams...)
Argument | Type | Obligatoire | Description |
---|---|---|---|
streams... | liste de Streams | Non | Liste de zéro ou plusieurs valeurs qui correspondent aux valeurs des streams passés à stream.lift . |
returns | any | Retourne une valeur calculée. |
Stream.SKIP
Une valeur spéciale qui peut être retournée par les callbacks de stream pour ignorer l'exécution des streams descendants.
Stream["fantasy-land/of"]
Cette méthode est fonctionnellement identique à stream
. Elle existe pour se conformer à la spécification Applicative de Fantasy Land. Pour plus d'informations, voir la section Qu'est-ce que Fantasy Land.
stream = Stream["fantasy-land/of"](value)
Argument | Type | Obligatoire | Description |
---|---|---|---|
value | any | Non | Si cet argument est présent, la valeur du stream lui est affectée. |
returns | Stream | Retourne un stream. |
Membres d'instance
stream.map
Crée un stream dépendant dont la valeur est définie sur le résultat de la fonction de callback. Cette méthode est un alias de stream["fantasy-land/map"].
dependentStream = stream().map(callback)
Argument | Type | Obligatoire | Description |
---|---|---|---|
callback | any -> any | Oui | Un callback dont la valeur de retour devient la valeur du stream. |
returns | Stream | Retourne un stream. |
stream.end
Un stream co-dépendant qui désenregistre les streams dépendants lorsqu'il est mis à true
. Voir la section État terminé.
endStream = stream().end
stream["fantasy-land/of"]
Cette méthode est fonctionnellement identique à stream
. Elle existe pour se conformer à la spécification Applicative de Fantasy Land. Pour plus d'informations, voir la section Qu'est-ce que Fantasy Land.
stream = stream()["fantasy-land/of"](value)
Argument | Type | Obligatoire | Description |
---|---|---|---|
value | any | Non | Si cet argument est présent, la valeur du stream lui est affectée. |
returns | Stream | Retourne un stream. |
stream["fantasy-land/map"]
Crée un stream dépendant dont la valeur est définie sur le résultat de la fonction de callback. Voir la section Chaînage de streams.
Cette méthode existe pour se conformer à la spécification Applicative de Fantasy Land. Pour plus d'informations, voir la section Qu'est-ce que Fantasy Land.
dependentStream = stream()["fantasy-land/map"](callback)
Argument | Type | Obligatoire | Description |
---|---|---|---|
callback | any -> any | Oui | Un callback dont la valeur de retour devient la valeur du stream. |
returns | Stream | Retourne un stream. |
stream["fantasy-land/ap"]
Le nom de cette méthode signifie apply
(appliquer). Si un stream a
a une fonction comme valeur, un autre stream b
peut l'utiliser comme argument pour b.ap(a)
. Appeler ap
appellera la fonction avec la valeur du stream b
comme argument, et retournera un autre stream dont la valeur est le résultat de l'appel de fonction. Cette méthode existe pour se conformer à la spécification Applicative de Fantasy Land. Pour plus d'informations, voir la section Qu'est-ce que Fantasy Land.
stream = stream()["fantasy-land/ap"](apply)
Argument | Type | Obligatoire | Description |
---|---|---|---|
apply | Stream | Oui | Un stream dont la valeur est une fonction. |
returns | Stream | Retourne un stream. |
Utilisation de base
Les streams ne font pas partie de la distribution principale de Mithril.js. Pour les inclure dans un projet, importez son module :
var stream = require('mithril/stream');
Streams comme variables
stream()
retourne un stream. À son niveau le plus basique, un stream fonctionne de manière similaire à une variable ou une propriété getter-setter : il peut contenir un état, qui peut être modifié.
var username = stream('John');
console.log(username()); // affiche "John"
username('John Doe');
console.log(username()); // affiche "John Doe"
La principale différence est qu'un stream est une fonction, et peut donc être composé avec des fonctions d'ordre supérieur.
var users = stream();
// Récupérer des utilisateurs depuis un serveur en utilisant l'API fetch
fetch('/api/users')
.then(function (response) {
return response.json();
})
.then(users);
Dans l'exemple ci-dessus, le stream users
est rempli avec les données de la réponse lorsque la requête est résolue.
Liaisons bidirectionnelles
Les streams peuvent également être remplis à partir de callbacks d'événements, etc.
// un stream
var user = stream('');
// une liaison bidirectionnelle au stream
m('input', {
oninput: function (e) {
user(e.target.value);
},
value: user(),
});
Dans l'exemple ci-dessus, lorsque l'utilisateur tape dans l'entrée, le stream user
est mis à jour avec la valeur du champ d'entrée.
Propriétés calculées
Les streams sont utiles pour implémenter des propriétés calculées :
var title = stream('');
var slug = title.map(function (value) {
return value.toLowerCase().replace(/\W/g, '-');
});
title('Hello world');
console.log(slug()); // affiche "hello-world"
Dans l'exemple ci-dessus, la valeur de slug
est calculée lorsque title
est mis à jour, et non lorsque slug
est lu.
Il est bien sûr également possible de calculer des propriétés basées sur plusieurs streams :
var firstName = stream('John');
var lastName = stream('Doe');
var fullName = stream.merge([firstName, lastName]).map(function (values) {
return values.join(' ');
});
console.log(fullName()); // affiche "John Doe"
firstName('Mary');
console.log(fullName()); // affiche "Mary Doe"
Les propriétés calculées dans Mithril.js sont mises à jour atomiquement : les streams qui dépendent de plusieurs streams ne seront jamais appelés plus d'une fois par mise à jour de valeur, quelle que soit la complexité du graphe de dépendance de la propriété calculée.
Chaînage de streams
Les streams peuvent être chaînés en utilisant la méthode map
. Un stream chaîné est également connu sous le nom de stream dépendant.
// stream parent
var value = stream(1);
// stream dépendant
var doubled = value.map(function (value) {
return value * 2;
});
console.log(doubled()); // affiche 2
Les streams dépendants sont réactifs : leurs valeurs sont mises à jour chaque fois que la valeur de leur stream parent est mise à jour. Cela se produit indépendamment du fait que le stream dépendant ait été créé avant ou après que la valeur du stream parent ait été définie.
Vous pouvez empêcher la mise à jour des streams dépendants en retournant la valeur spéciale stream.SKIP
.
var skipped = stream(1).map(function (value) {
return stream.SKIP;
});
skipped.map(function () {
// ne s'exécute jamais
});
Combinaison de streams
Les streams peuvent dépendre de plus d'un stream parent. Ces types de streams peuvent être créés via 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()); // affiche "hello world"
Ou vous pouvez utiliser la fonction d'assistance 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()); // affiche "hello world"
Il existe également une méthode de niveau inférieur appelée stream.combine()
qui expose les streams eux-mêmes dans les calculs réactifs pour des cas d'utilisation plus avancés.
var a = stream(5);
var b = stream(7);
var added = stream.combine(
function (a, b) {
return a() + b();
},
[a, b]
);
console.log(added()); // affiche 12
Un stream peut dépendre d'un nombre quelconque de streams et il est garanti de se mettre à jour atomiquement. Par exemple, si un stream A a deux streams dépendants B et C, et qu'un quatrième stream D est dépendant à la fois de B et de C, le stream D ne se mettra à jour qu'une seule fois si la valeur de A change. Cela garantit que le callback pour le stream D n'est jamais appelé avec des valeurs incohérentes, par exemple lorsque B a une nouvelle valeur mais que C a l'ancienne valeur. L'atomicité apporte également les avantages de performance de ne pas recalculer inutilement les flux descendants.
Vous pouvez empêcher la mise à jour des streams dépendants en retournant la valeur spéciale stream.SKIP
.
var skipped = stream.combine(
function (stream) {
return stream.SKIP;
},
[stream(1)]
);
skipped.map(function () {
// ne s'exécute jamais
});
États de stream
À un moment donné, un stream peut être dans l'un des trois états : en attente, actif et terminé.
État en attente
Les streams en attente peuvent être créés en appelant stream()
sans paramètres.
var pending = stream();
Si un stream dépend de plus d'un stream et que l'un de ses streams parents est dans un état en attente, le stream dépendant est également dans un état en attente et ne met pas à jour sa valeur.
var a = stream(5);
var b = stream(); // stream en attente
var added = stream.combine(
function (a, b) {
return a() + b();
},
[a, b]
);
console.log(added()); // affiche undefined
Dans l'exemple ci-dessus, added
est un stream en attente, car son parent b
est également en attente.
Cela s'applique également aux streams dépendants créés via stream.map
:
var value = stream();
var doubled = value.map(function (value) {
return value * 2;
});
console.log(doubled()); // affiche undefined car `doubled` est en attente
État actif
Lorsqu'un stream reçoit une valeur, il devient actif (sauf si le stream est terminé).
var stream1 = stream('hello'); // stream1 est actif
var stream2 = stream(); // stream2 commence en attente
stream2('world'); // puis devient actif
Un stream dépendant avec plusieurs parents devient actif si tous ses parents sont actifs.
var a = stream('hello');
var b = stream();
var greeting = stream.merge([a, b]).map(function (values) {
return values.join(' ');
});
Dans l'exemple ci-dessus, le stream a
est actif, mais b
est en attente. Définir b("world")
ferait que b
devienne actif, et donc greeting
deviendrait également actif, et serait mis à jour pour avoir la valeur "hello world"
.
État terminé
Un stream peut cesser d'affecter ses streams dépendants en appelant stream.end(true)
. Cela supprime effectivement la connexion entre un stream et ses streams dépendants.
var value = stream();
var doubled = value.map(function (value) {
return value * 2;
});
value.end(true); // défini à l'état terminé
value(5);
console.log(doubled());
// affiche undefined car `doubled` ne dépend plus de `value`
Les streams terminés ont toujours une sémantique de conteneur d'état, c'est-à-dire que vous pouvez toujours les utiliser comme getter-setters, même après qu'ils soient terminés.
var value = stream(1);
value.end(true); // défini à l'état terminé
console.log(value(1)); // affiche 1
value(2);
console.log(value()); // affiche 2
Mettre fin à un stream peut être utile dans les cas où un stream a une durée de vie limitée (par exemple, réagir aux événements mousemove
uniquement pendant qu'un élément DOM est en cours de glissement, mais pas après qu'il ait été déposé).
Sérialisation des streams
Les streams implémentent une méthode .toJSON()
. Lorsqu'un stream est passé comme argument à JSON.stringify()
, la valeur du stream est sérialisée.
var value = stream(123);
var serialized = JSON.stringify(value);
console.log(serialized); // affiche 123
Les streams ne déclenchent pas le rendu
Contrairement aux bibliothèques comme Knockout, les streams Mithril.js ne déclenchent pas le re-rendu des modèles. Le redessin se produit en réponse aux gestionnaires d'événements définis dans les vues de composants Mithril.js, aux changements de route ou après la résolution des appels m.request
.
Si un redessin est souhaité en réponse à d'autres événements asynchrones (par exemple, setTimeout
/setInterval
, abonnement websocket, gestionnaire d'événements de bibliothèque tierce, etc.), vous devez appeler manuellement m.redraw()
.
Qu'est-ce que Fantasy Land
Fantasy Land spécifie l'interopérabilité des structures algébriques courantes. En termes simples, cela signifie que les bibliothèques qui se conforment aux spécifications de Fantasy Land peuvent être utilisées pour écrire du code de style fonctionnel générique qui fonctionne indépendamment de la façon dont ces bibliothèques implémentent les constructions.
Par exemple, disons que nous voulons créer une fonction générique appelée plusOne
. L'implémentation naïve ressemblerait à ceci :
function plusOne(a) {
return a + 1;
}
Le problème avec cette implémentation est qu'elle ne peut être utilisée qu'avec un nombre. Cependant, il est possible que la logique qui produit une valeur pour a
puisse également produire un état d'erreur (encapsulé dans un Maybe ou un Either d'une bibliothèque comme Sanctuary ou Ramda-Fantasy), ou il pourrait s'agir d'un stream Mithril.js, d'un stream Flyd, etc. Idéalement, nous ne voudrions pas écrire une version similaire de la même fonction pour chaque type possible que a
pourrait avoir et nous ne voudrions pas écrire de code d'encapsulation/déencapsulation/gestion des erreurs à plusieurs reprises.
C'est là que Fantasy Land peut aider. Réécrivons cette fonction en termes d'algèbre de Fantasy Land :
var fl = require('fantasy-land');
function plusOne(a) {
return a[fl.map](function (value) {
return value + 1;
});
}
Maintenant, cette méthode fonctionne avec n'importe quel Functor conforme à Fantasy Land, tel que R.Maybe
, S.Either
, stream
, etc.
Cet exemple peut sembler alambiqué, mais c'est un compromis en termes de complexité : l'implémentation naïve plusOne
a du sens si vous avez un système simple et que vous n'incrémentez que des nombres, mais l'implémentation de Fantasy Land devient plus puissante si vous avez un grand système avec de nombreuses abstractions d'encapsulation et des algorithmes réutilisés.
Lorsque vous décidez si vous devez adopter Fantasy Land, vous devez tenir compte de la familiarité de votre équipe avec la programmation fonctionnelle et être réaliste quant au niveau de discipline que votre équipe peut s'engager à maintenir la qualité du code (par rapport à la pression d'écrire de nouvelles fonctionnalités et de respecter les délais). La programmation de style fonctionnel dépend fortement de la compilation, de la conservation et de la maîtrise d'un grand ensemble de petites fonctions définies avec précision, et n'est donc pas adaptée aux équipes qui n'ont pas de pratiques de documentation solides et/ou qui manquent d'expérience dans les langages orientés fonctionnels.