lunes, 9 de marzo de 2020

Composición de software - Canalización de funciones con aridad heterogénea en JavaScript usando mónadas.

La cosa viene de la I parte. Pégale un vistazo si no lo has hecho todavía.

En esta entrada voy a usar una unión disyuntiva en las funciones orquestables del ejemplo anterior y modificamos la función pipe para que opere con ella.


La unión disyuntiva (mónada Either de aquí en adelante) es un tipo de constructo que puede representar 2 tipos de valores pero solo uno de ellos a la vez. Either se divide en parte derecha y parte izquierda; las podemos usar indiscriminadamente para representar lo que queramos. Lo más común cuando representamos un OK y un Fail con esta mónada es; al igual que en los callback de JavaScript; utilizar la parte izquierda para representar el Fail y la derecha para el OK.

Utilizando correctamente sus operaciones podemos enlazar las funciones del workflow configuradas en la función pipe(...fncs) únicamente cuando el tipo contenido en la mónada Either sea su parte derecha. En caso de contener la parte izquierda, se saltaría el enlace y retornaría la parte izquierda sin modificar; esto implica que si sigues haciendo llamadas de enlace de la parte derecha posteriormente; su salida final sería la parte izquierda sin modificar.

//module Rules
function isPlayerTurn(gameState, player) {
  return gameState.currentPlayerTurn === player;
}
 
function operationCost(operation) {
  return 2//fake value for example purposes
}
 
function playerHasEnergy(gameState, cost) {
  return gameState.currentPlayerTurn.energy >= cost;
}
 
function executeGameOperation(gameState, operation, target) {
  //make changes needed for the operation and target
  return gameState;
}
 
function endGamePlayerTurn(gameState, player) {
  //make changes for end turn
  return gameState;
}
 
//module orchestation code, now it returns Either monads. right -> ok, left -> fail
const checkIsNotPlayerTurn = ({ gameState, player }) =>
  !isPlayerTurn(gameState, player) ?
    Either.left("it is not player turn.":
    Either.right({});
 
const obtainOperationCost = ({ operation }) =>
  Either.right({ cost: operationCost(operation) });
 
const checkNotEnoughEnergy = ({ gameState, cost }) =>
  !playerHasEnergy(gameState, cost) ?
    Either.left("player has not enough energy for requested operation":
    Either.right({});
 
const executeOperation = ({ gameState, operation, target }) =>
  Either.right({ gameState: executeGameOperation(gameState, operation, target) }); //will modify gameState

const endTurn = ({ gameState, player }) =>
  Either.right({ gameState: endGamePlayerTurn(gameState, player)} ); //will modify gameState

//now we do not need until checks for shortcircuit   
function pipe(...fns) {
 
  return (input) => {
    valueOrError = Either.right(input);
    for (fnc of fns) {
/* keeps flatMap all the right parts; 
 * if some fnc return left the rest of the flatMaps are ignored*/
      valueOrError = valueOrError.flatMap(value => fnc(Object.assign(input, value)));
    }
    return valueOrError.map((value) => Object.assign(input, value));
  };
}
 
const executeOperationWorkFlow = pipe(
  checkIsNotPlayerTurn
  , obtainOperationCost
  , checkNotEnoughEnergy
  , executeOperation
);
 
const endTurnWorkFlow = pipe(
  checkIsNotPlayerTurn //reusing orchestation fncs for any workflow we need
  , endTurn
);
 
//how to use
let result = executeOperationWorkFlow({ gameState, player, operation, target });
result.cata(
   /*
    * outputs:    "Failed because it is not player turn" 
                                  OR 
               "Failed because player has not enough energy...."
   */
  failure => `Failed because ${failure}`,
  success => `Great!. New state of the game after the operation ${success.gameState}`
);
 
 
result = endTurnWorkFlow({ gameState, player });
result.cata(
  //outputs: Failed because it is not player turn
  failure => `Failed because ${failure}`,
  success => `Great!. New state of the game after the operation ${success.gameState}`
);


Repasemos como nos ha quedado:

- Cortocircuitado del work flow automático.
- Obtención de la razón del cortocircuitado y posibilidad de actuar en consecuencia sin necesidad de estructuras de control condicional.
- Hemos eliminado todas las estructuras de control condicional en el código de orquestación. Gracias a esto hemos reducido la complejidad ciclomática drásticamente. Esto significa código mas sencillo, mas fácil de leer, menos posibles bugs y por lo tanto una gran mantenibilidad.

El engine del juego completo que hice; del que estoy sacando estos ejemplos; tiene exactamente 1 else en más de 1260 líneas de código por las que puede pasar el flujo de ejecución cada vez que el usuario realiza una operación y el if  más complejo que tengo se compone de tan solo 3 líneas de código. Es verdad que, además de esta técnica de composición de software, me estoy apoyando en otras técnicas como las extensiones reactivas para eliminar condiciones de flujo de control pero casi puedo asegurar que esta técnica me ha quitado perfectamente el 50% de ellas. Y al fin y al cabo, las extensiones reactivas son una mónada tambien :-D

No hay comentarios:

Publicar un comentario