3
votes

Comment affecter conditionnellement à une variable de manière fonctionnelle?

Je dois attribuer une valeur à une variable basée sur une condition. Je veux le faire avec un paradigme de programmation fonctionnel à l'esprit, donc je ne peux pas le déclarer dans la portée externe puis le réaffecter.

if(condition) {
  const foo = 1
  do_stuff()
} else {
  const foo = 0
  do_other_stuff()
}
do_third_stuff(foo)

Je sais, je peux utiliser une expression ternaire ici comme
const foo = condition? 1: 0
mais que faire si j'ai une autre routine à faire dans ma condition, comme:

// let foo = undefined // no-let!
if(condition) {
  const foo = 1
} else {
  const foo = 0
}
do_some_stuff(foo) // Uncaught ReferenceError: foo is not defined


5 commentaires

Qu'en est-il de la composition fonctionnelle si vous désirez un style fonctionnel?


fournir un exemple plz


Vous êtes toujours coincé dans une réflexion impérative. Vos fonctions ne reçoivent ni ne retournent de valeur. Ils sont complètement inutiles. La programmation fonctionnelle ne consiste pas seulement à utiliser des fonctions, mais à les utiliser de manière pure. Une fonction pure prend une valeur et renvoie une valeur transformée. Rien d'autre. Ensuite, vous reconnaîtrez que vous pouvez composer de telles fonctions pures, ce qui réduit considérablement le besoin de valeurs intermédiaires affectées aux variables.


Que sont censés fournir do_stuff et do_other_stuff ? Il ne renvoie pas une valeur que vous utilisez, il s'agit donc d'effets secondaires ou de code mort. Si ce sont des effets secondaires, cela n'aide pas à changer un let en const


@reify devrait penser à une solution comme ma réponse récemment ajoutée.


3 Réponses :


2
votes

Rien ne vous empêche de séparer les deux:

let isFoo =  expensiveIsFooMethod();

const foo = isFoo ? 1 : 0;

if(isFoo) {
    do_stuff();
} else {
    do_other_stuff();
}

do_third_stuff(foo);

Dans le cas où la condition est une exécution coûteuse, affectez-la simplement à une variable avant de l'utiliser plusieurs fois:

const foo = condition ? 1 : 0;

if(condition) {
    do_stuff();
} else {
    do_other_stuff();
}

do_third_stuff(foo);

Vous avez raison de dire que ce serait plus propre si vous n'aviez pas à répéter la condition, mais vous avez introduit cette limitation car vous utilisez un const qui rend impossible d'attribuer une valeur à votre const à plus d'un endroit.

Je suggère de l'emporter sur les deux options ici. Qu'est-ce qui compte le plus pour vous: une syntaxe plus claire ou vous assurer de ne jamais écraser la valeur?


1 commentaires

Voir le commentaire de @ reify sur la question



0
votes

Parce que vous ne voulez pas déclarer fou à l'extérieur, pourquoi vous ne le faites pas simplement de cette façon:

if(condition) {
  const foo = 1
  do_stuff()
  do_third_stuff(foo)
} else {
  const foo = 0
  do_other_stuff()
  do_third_stuff(foo)
}


1 commentaires

Surtout si do_third_stuff () est plus d'une ligne, cela viole DRY et peut conduire à un code difficile à maintenir, ce qui à son tour conduit à une forte probabilité de bogues lors de l'introduction de changements (par exemple, en changeant le code < > if mais en oubliant de changer également le else )



3
votes

Vous encoderiez probablement votre cas en utilisant un type de données algébrique (ADT) comme Soit . Autrement dit, vous pouvez couvrir deux sous-cas: gauche et droite .

Voir le code à partir de // -> La solution commence ici et plus . Le code précédent est une mini bibliothèque FP standard utilisant du JavaScript vanilla pour rendre le code exécutable. Vérifiez-le et profitez-en!

// Mini standard library
// -------------------------------

// The identity combinator
// I :: a -> a
const I = x => x

// Pipes many unary functions
//
// pipe :: [a -> b] -> a -> c
const pipe = xs => x => xs.reduce ((o, f) => f (o), x)

// Either ADT
const Either = (() => {
   // Creates an instance of Either.Right
   //
   // of :: b -> Either a b
   const of = x => ({ right: x })
   
   // Creates an instance of Either.Right
   //
   // Right :: b -> Either a b
   const Right = of
   
   // Creates an instance of Either.Left
   //
   // Left :: a -> Either a b
   const Left = x => ({ left: x })
   
   // Maps Either.Left or Either.Right in a single operation
   //
   // bimap :: (a -> c) -> (b -> d) -> Either a b -> Either c -> d
   const bimap = f => g => ({ left, right }) => left ? Left (f (left)) : Right (g (right))
   
   // Lifts a value to Either based on a condition, where false 
   // results in Left, and true is Right.
   //
   // tagBy :: (a -> Boolean) -> a -> Either a a
   const tagBy = f => x => f (x) ? Right (x) : Left (x)
   
   // Unwraps Either.Left or Either.Right with mapping functions
   //
   // either :: (a -> c) -> (b -> c) -> Either a b -> c
   const either = f => g => ({ left, right }) => left ? f (left) : g (right)
   
   // Unwraps Either.Left or Either.Right and outputs the raw value on them
   //
   // unwrap :: Either a b -> c
   const unwrap = either (I) (I)
   
   return { of, Right, Left, bimap, tagBy, either, unwrap }
}) ()

// --> Solution starts here

// doStuff :: Number -> Number
const doStuff = x => x + 1

// doStuff2 :: Number -> Number
const doStuff2 = x => x * 4

const { tagBy, bimap, unwrap } = Either

// doStuff3 :: Number -> Number
const doStuff3 = pipe ([
   tagBy (x => x > 3),
   bimap (doStuff) (doStuff2), // <-- here's the decision!
   unwrap
])

const output1 = doStuff3 (2)
const output2 = doStuff3 (30)

console.log ('output1: ', output1)
console.log ('output2: ', output2)

Refactoriser à l'aide de tuyaux

Maintenant, j'introduis pipe pour coller une composition d'une ou plusieurs fonctions unaires, ce qui rend le code plus élégant:

// Mini standard library
// -------------------------------

// The identity combinator
// I :: a -> a
const I = x => x

// Either ADT
const Either = (() => {
   // Creates an instance of Either.Right
   //
   // of :: b -> Either a b
   const of = x => ({ right: x })
   
   // Creates an instance of Either.Right
   //
   // Right :: b -> Either a b
   const Right = of
   
   // Creates an instance of Either.Left
   //
   // Left :: a -> Either a b
   const Left = x => ({ left: x })
   
   // Maps Either.Left or Either.Right in a single operation
   //
   // bimap :: (a -> c) -> (b -> d) -> Either a b -> Either c -> d
   const bimap = f => g => ({ left, right }) => left ? Left (f (left)) : Right (g (right))
   
   // Lifts a value to Either based on a condition, where false 
   // results in Left, and true is Right.
   //
   // tagBy :: (a -> Boolean) -> a -> Either a a
   const tagBy = f => x => f (x) ? Right (x) : Left (x)
   
   // Unwraps Either.Left or Either.Right with mapping functions
   //
   // either :: (a -> c) -> (b -> c) -> Either a b -> c
   const either = f => g => ({ left, right }) => left ? f (left) : g (right)
   
   // Unwraps Either.Left or Either.Right and outputs the raw value on them
   //
   // unwrap :: Either a b -> c
   const unwrap = either (I) (I)
   
   return { of, Right, Left, bimap, tagBy, either, unwrap }
}) ()

// --> Solution starts here

// Lifts to Either.Right if x is greater than 3, 
// otherwise, x is encoded as Left.
//
// tagGt3 :: Number -> Either Number Number
const tagGt3 = Either.tagBy (x => x > 3)

// doStuff :: Number -> Number
const doStuff = x => x + 1

// doStuff2 :: Number -> Number
const doStuff2 = x => x * 4

// doStuff3 :: Either Number Number -> Either Number Number
const doStuff3 = Either.bimap (doStuff) (doStuff2) // <-- here's the decision!

const eitherValue1 = doStuff3 (tagGt3 (2))
const eitherValue2 = doStuff3 (tagGt3 (30))


const output1 = Either.unwrap (eitherValue1)
const output2 = Either.unwrap (eitherValue2)

console.log ('output1: ', output1)
console.log ('output2: ', output2)


10 commentaires

Dernièrement, j'ai tendance à être aussi explicite que possible avec les conditions en les enveloppant simplement dans des fonctions: const _let = f => f (); _let ((x = someParentScopeVar) => {if (x) return doStuff (x); else return doOtherStuff (x)}) . En abusant des paramètres par défaut de cette façon, le flux de données est explicite. De plus, si au lieu de someParentScopeVar vous passez une expression extensive dont le résultat est utilisé plusieurs fois, vous obtenez une sorte de liaison let .


@reify Ce commentaire est-il au bon endroit? Peut-être que vous vouliez ajouter cette information à la question d'OP: O


Le commentaire @reify est un peu hors sujet pour cet article, mais je pense aussi que c'est une bonne utilisation des arguments par défaut. Je suggère bind , where ou assign comme noms alternatifs pour _let .


@reify Maintenant, je me demande si cela devrait être intégré à la boucle / recur que j'écris souvent. Je commence à ressembler beaucoup au named-let de Racket et c'est vraiment une bonne chose :RÉ


@ user633183 Merci! J'essaye de divulguer FP et de me défier: D


@ user633183 J'ai écrit ce commentaire parce que je pense que le type de base Either est principalement utilisé à cause de sa contrainte de monade, où left indique une erreur, pas pour encoder des branches conditionnelles. Quoi qu'il en soit, merci pour le lien de raquette. Je vais l'examiner.


@reify Pas vraiment. Soit peut être utilisé pour quitter une composition Kleisli à un certain point en sortie Gauche , et continuer avec Droite .


Correct, utiliser Soit pour signaler des erreurs n'est qu'un un cas d'utilisation possible (mais populaire).


@ user633183 Ouais.


@ user633183 De plus, un cas réel n'impliquerait probablement pas l'extraction de la valeur de Soit comme dans mon exemple. La composition entière consisterait à transformer de / en Soit et d'autres types de données ... Dans un cas très simple comme les OP, cela pourrait sembler exagéré, mais je suppose que l'OP et les futurs lecteurs pourraient Prenez l'astuce pour produire des solutions plus complexes avec Soit .