2

Une tâche a quelques étapes, si l'entrée de chaque étape est seulement de la dernière étape directe, c'est facile. Cependant, plus souvent, certaines étapes dépendent non seulement de la dernière étape directe.En utilisant la programmation fonctionnelle javascript avec folktale2, comment accéder gracieusement aux résultats des tâches précédentes?

Je peux m'entraîner de plusieurs manières, mais toutes finissent par avoir un code imbriqué laid, j'espère que n'importe qui pourrait m'aider à trouver de meilleurs moyens.

J'ai créé l'exemple signIn comme suit pour démontrer, le processus a 3 étapes ci-dessous:

  1. connexion de base de données get (() -> Connexion de la tâche)
  2. find compte (connexion -> Tâche compte)
  3. créer jeton (Connexion -> accountId -> Token Tâche)

# étape3 dépend non seulement de l'étape 2, mais aussi l'étape # 1.

Le ci-dessous sont les tests unitaires de plaisanterie en utilisant folktale2

import {task, of} from 'folktale/concurrency/task' 
import {converge} from 'ramda' 

const getDbConnection =() => 
    task(({resolve}) => resolve({id: `connection${Math.floor(Math.random()* 100)}`}) 
) 

const findOneAccount = connection => 
    task(({resolve}) => resolve({name:"ron", id: `account-${connection.id}`})) 

const createToken = connection => accountId => 
    task(({resolve}) => resolve({accountId, id: `token-${connection.id}-${accountId}`})) 

const liftA2 = f => (x, y) => x.map(f).ap(y) 

test('attempt#1 pass the output one by one till the step needs: too many passing around', async() => { 
    const result = await getDbConnection() 
     .chain(conn => findOneAccount(conn).map(account => [conn, account.id])) // pass the connection to next step 
     .chain(([conn, userId]) => createToken(conn)(userId)) 
     .map(x=>x.id) 
     .run() 
     .promise() 

    console.log(result) // token-connection90-account-connection90 
}) 

test('attempt#2 use ramda converge and liftA2: nested ugly', async() => { 
    const result = await getDbConnection() 
     .chain(converge(
      liftA2(createToken), 
      [ 
       of, 
       conn => findOneAccount(conn).map(x=>x.id) 
      ] 
     )) 
     .chain(x=>x) 
     .map(x=>x.id) 
     .run() 
     .promise() 

    console.log(result) // token-connection59-account-connection59 
}) 

test('attempt#3 extract shared steps: wrong', async() => { 
    const connection = getDbConnection() 

    const accountId = connection 
    .chain(conn => findOneAccount(conn)) 
    .map(result => result.id) 

    const result = await of(createToken) 
    .ap(connection) 
    .ap(accountId) 
    .chain(x=>x) 
    .map(x=>x.id) 
    .run() 
    .promise() 

    console.log(result) // token-connection53-account-connection34, wrong: get connection twice 
}) 
  • tentative # 1 est juste, mais je dois passer la sortie de l'étape très tôt jusqu'à ce que les étapes ont besoin, si elle est à travers de nombreuses étapes, il est très ennuyeux.

  • La tentative n ° 2 a également raison, mais se termine par un code imbriqué. J'aime la tentative # 3, elle utilise une certaine variable pour contenir la valeur, mais malheureusement, cela ne fonctionne pas.

Mise à jour-1 Je suis pense qu'une autre façon de mettre toutes les sorties dans un état qui passera à travers, mais il peut très semblable tentative # 1

test.only('attempt#4 put all outputs into a state which will pass through', async() => { 
    const result = await getDbConnection() 
    .map(x=>({connection: x})) 
    .map(({connection}) => ({ 
     connection, 
     account: findOneAccount(connection) 
    })) 
    .chain(({account, connection})=> 
     account.map(x=>x.id) 
     .chain(createToken(connection)) 
    ) 
    .map(x=>x.id) 
    .run() 
    .promise() 


    console.log(result) //  token-connection75-account-connection75 
}) 

update-2 En utilisant l'approche do de @ Scott, je suis plutôt satisfait de l'approche ci-dessous. C'est court et propre.

test.only('attempt#5 use do co', async() => { 
    const mdo = require('fantasy-do') 

    const app = mdo(function *() { 
     const connection = yield getDbConnection() 
     const account = yield findOneAccount(connection) 

     return createToken(connection)(account.id).map(x=>x.id) 
    }) 

    const result = await app.run().promise() 

    console.log(result) 
}) 
+1

Je suppose que la question est de savoir comment accéder aux résultats des tâches précédentes, non? Faire cela en passant explicitement un objet d'état à travers la chaîne n'est pas si mauvais. De plus, vous pouvez combiner 'tâche' avec un transformateur monadique' lecteurT' (ou avec 'stateT' si vous avez besoin d'une mutation) pour faire abstraction de cet objet. Mais je ne suis pas sûr que cette abstraction en vaille la peine, ni que vous puissiez implémenter un lecteur correct avec le système prototype de Javacript. – ftor

+0

@ftor, je pense que j'ai besoin d'apprendre un peu de haskell pour comprendre la monade 'lecteurT', je suppose qu'il peut aimer un état centralisé. Je vais regarder dans 'lecteurT' et' stateT'. Merci beaucoup pour cette information. – Ron

+0

Relié, sinon dupliqué de [Comment accéder aux résultats de la promesse précédente dans une chaîne .then()? (Https://stackoverflow.com/q/28250680/1048572) - alors que 'Task's ne sont pas désireux, leur l'interface monadique et la combinaison «parallèle» sont exactement équivalentes aux promesses. – Bergi

Répondre

2

Votre exemple pourrait être écrit comme suit:

const withConnection = connection => 
    findOneAccount(connection) 
     .map(x => x.id) 
     .chain(createToken(connection)) 

getDbConnection().chain(withConnection) 

Ceci est similaire à votre deuxième tentative, fait que l'utilisation de chain plutôt que ap/lift pour éliminer la nécessité pour la chain(identity) ultérieure. Cela pourrait également être mis à jour pour utiliser converge si vous voulez, bien que je pense qu'il perd beaucoup de lisibilité dans le processus.

const withConnection = R.converge(R.chain, [ 
    createToken, 
    R.compose(R.map(R.prop('id')), findOneAccount) 
]) 

getDbConnection().chain(withConnection) 

Il pourrait également être mis à jour pour ressembler à votre troisième tentative avec l'utilisation de générateurs. La définition suivante de la fonction Do pourrait être remplacée par l'une des bibliothèques existantes offrant une forme de "syntaxe".

// sequentially calls each iteration of the generator with `chain` 
const Do = genFunc => { 
    const generator = genFunc() 
    const cont = arg => { 
    const {done, value} = generator.next(arg) 
    return done ? value : value.chain(cont) 
    } 
    return cont() 
} 

Do(function*() { 
    const connection = yield getDbConnection() 
    const account = yield findOneAccount(connection) 
    return createToken(connection)(account.id) 
}) 
+0

merci pour votre contribution. le premier ensemble de code semble propre mais imbriqué, j'ai convenu que le deuxième jeu de code n'est pas très lisible. Le dernier sonne bien, il aime «co» dans le monde de la «promesse», intéressant, bien qu'il n'y en ait pas un de tel. – Ron

+1

@Ron J'ai édité l'exemple pour extraire la continuation imbriquée dans sa propre fonction 'withConnection', bien qu'elle ne fasse que réduire superficiellement l'imbrication. J'ai également inclus une implémentation simple de 'Do' pour vous, bien qu'il existe des bibliothèques existantes qui offrent déjà cette fonctionnalité ainsi que https://github.com/jwoudenberg/fantasy-do et https://github.com/pelotom/burrido –

+0

merci pour vos mises à jour. Bien que je sente toujours que le jeu de codes mis à jour est imbriqué, j'aime votre approche 'do' avec deux bibliothèques, c'est court et propre, j'ai déjà mis à jour le test d'unité passé dans mon post original. – Ron