Les fonctions qui prennent des fonctions comme paramètres devraient-elles également prendre des paramètres pour ces fonctions comme paramètres?

20

Je me retrouve souvent à écrire des fonctions qui ressemblent à ceci parce qu'elles me permettent de simuler facilement l'accès aux données, tout en fournissant une signature qui accepte les paramètres pour déterminer les données auxquelles accéder.

public static string GetFormattedRate(
        Func<string, RateType>> getRate,
        string rateKey)
{
    var rate = getRate(rateKey);
    var formattedRate = rate.DollarsPerMonth.ToString("C0");
    return formattedRate;
}

Ou

public static string GetFormattedRate(
        Func<RateType, string> formatRate,
        Func<string, RateType>> getRate,
        string rateKey)
{
    var rate = getRate(rateKey);
    var formattedRate = formatRate(rate);
    return formattedRate;
}

Ensuite, je l'utilise quelque chose comme ceci:

using FormatterModule;

public static Main()
{
    var getRate = GetRateFunc(connectionStr);
    var formattedRate = GetFormattedRate(getRate, rateType);
    // or alternatively
    var formattedRate = GetFormattedRate(getRate, FormatterModule.FormatRate, rateKey);

    System.PrintLn(formattedRate);
}

Est-ce une pratique courante? Je sens que je devrais faire quelque chose de plus

public static string GetFormattedRate(
        Func<RateType> getRate())
{
    var rate = getRate();
    return rate.DollarsPerMonth.ToString("C0");
}

Mais cela ne semble pas très bien fonctionner car je devrais créer une nouvelle fonction pour passer dans la méthode pour chaque type de taux.

Parfois, je sens que je devrais faire

public static string GetFormattedRate(RateType rate)
{
   return rate.DollarsPerMonth.ToString("C0");
}

Mais cela semble supprimer toute possibilité de récupération et de formatage. Chaque fois que je veux récupérer et formater, je dois écrire deux lignes, une à récupérer et une à formater.

Qu'est-ce qui me manque dans la programmation fonctionnelle? Est-ce la bonne façon de le faire ou existe-t-il un meilleur modèle à la fois facile à entretenir et à utiliser?

se précipiter
la source
50
Le cancer DI s'est propagé jusqu'à présent ...
Idan Arye
16
J'ai du mal à voir pourquoi cette structure serait utilisée en premier lieu. Il est sûrement plus pratique (et clair ) GetFormattedRate()d'accepter le taux à formater comme paramètre, plutôt que de lui faire accepter une fonction qui renvoie le taux à formater comme paramètre?
2017
6
Une meilleure façon est d'utiliser l' closuresendroit où vous passez le paramètre lui-même à une fonction, ce qui en retour vous donne une fonction faisant référence à ce paramètre spécical. Cette fonction "configurée" serait passée en paramètre à la fonction qui l'utilise.
Thomas Junk
7
@IdanArye DI cancer?
Jules
11
@Julence dependency injection cancer
cat

Réponses:

39

Si vous faites cela assez longtemps, vous finirez par vous retrouver à écrire cette fonction encore et encore:

public static Type3 CombineFunc1AndFunc2(
    Func<Type1, Type2> func1,
    Func<Type2, Type3>> func2,
    Type1 input)
{
    return func2(func1(input))
}

Félicitations, vous avez inventé la composition des fonctions .

Les fonctions d'encapsuleur comme celle-ci n'ont pas beaucoup d'utilité lorsqu'elles sont spécialisées dans un seul type. Cependant, si vous introduisez des variables de type et omettez le paramètre d'entrée, votre définition GetFormattedRate ressemble à ceci:

public static Func<A, C> Compose(
    Func<B, C> outer, Func<A, B>> inner)
{
    return (input) => outer(inner(input))
}

var GetFormattedRate = Compose(FormatRate, GetRate);
var formattedRate = GetFormattedRate(rateKey);

Dans l'état actuel des choses, ce que vous faites a peu d'utilité. Ce n'est pas générique, vous devez donc dupliquer ce code partout. Il sur-complique votre code car maintenant votre code doit assembler tout ce dont il a besoin à partir de mille petites fonctions. Mais votre cœur est au bon endroit: il vous suffit de vous habituer à utiliser ces sortes de fonctions génériques d'ordre supérieur pour assembler les choses. Ou, utilisez un bon lambda à l'ancienne pour transformer Func<A, B>et Adevenir Func<B>.

Ne te répète pas.

Jack
la source
16
Répétez-vous si éviter de vous répéter aggrave le code. Comme si vous écrivez toujours ces deux lignes au lieu de FormatRate(GetRate(rateKey)).
user253751
6
@immibis Je suppose que l'idée est qu'il pourra GetFormattedRatedésormais utiliser directement.
Carles
Je pense que c'est ce que j'essaie de faire ici, et j'ai déjà essayé cette fonction de composition, mais il semble que j'arrive rarement à l'utiliser car ma 2ème fonction a souvent besoin de plus d'un paramètre. Peut-être que je dois le faire en combinaison avec des fermetures pour les fonctions configurées comme mentionné par @ thomas-junk
rushinge
@rushinge Ce type de composition fonctionne sur la fonction FP typique qui a toujours un seul argument (les arguments supplémentaires sont vraiment des fonctions qui leur sont propres, pensez-y comme Func<Func<A, B>, C>); cela signifie que vous n'avez besoin que d'une seule fonction de composition qui fonctionne pour n'importe quelle fonction. Cependant, vous pouvez assez bien travailler avec les fonctions C # en utilisant simplement des fermetures - au lieu de passer Func<rateKey, rateType>, vous avez vraiment besoin Func<rateType>, et lorsque vous passez le func, vous le construisez comme () => GetRate(rateKey). Le fait est que vous n'exposez pas les arguments dont la fonction cible ne se soucie pas.
Luaan
1
@immibis Oui, la Composefonction n'est vraiment utile que si vous devez retarder l'exécution de GetRatepour une raison quelconque, comme si vous souhaitez passer Compose(FormatRate, GetRate)à une fonction qui fournit un taux de son choix, par exemple pour l'appliquer à chaque élément d'un liste.
jpaugh
107

Il n'y a absolument aucune raison de passer une fonction et ses paramètres, puis de l'appeler avec ces paramètres. En fait, dans votre cas , vous avez aucune raison de passer une fonction du tout . L'appelant pourrait tout aussi bien appeler la fonction elle-même et transmettre le résultat.

Pensez-y - au lieu d'utiliser:

var formattedRate = GetFormattedRate(getRate, rateType);

pourquoi ne pas simplement utiliser:

var formattedRate = GetFormattedRate(getRate(rateType));

?

En plus de réduire le code inutile, il réduit également le couplage - si vous voulez changer la façon dont le taux est récupéré (par exemple, s'il a getRatemaintenant besoin de deux arguments), vous n'avez pas à le changer GetFormattedRate.

De même, il n'y a aucune raison d'écrire GetFormattedRate(formatRate, getRate, rateKey)au lieu d'écrire formatRate(getRate(rateKey)).

Ne compliquez pas les choses.

user253751
la source
3
Dans ce cas, vous avez raison. Mais si la fonction interne était appelée plusieurs fois, disons dans une boucle ou une fonction de carte, alors la possibilité de passer des arguments serait utile. Ou utilisez la composition fonctionnelle / curry comme proposé dans la réponse @Jack.
user949300
15
@ user949300 peut-être, mais ce n'est pas le cas d'utilisation de l'OP (et si c'était le cas, c'est peut-être cela formatRatequi devrait être mappé sur les taux qui devraient être formatés).
jonrsharpe
4
@ user949300 Uniquement si votre langue ne prend pas en charge les fermetures ou lorsque les lamdas sont difficiles à taper
Bergi
4
Notez que passer une fonction et ses paramètres à une autre fonction est un moyen totalement valable de retarder l'évaluation dans un langage sans sémantique paresseuse en.wikipedia.org/wiki/Thunk
Jared Smith
4
@JaredSmith En passant la fonction, oui, en passant ses paramètres, seulement si votre langue ne supporte pas les fermetures.
user253751
15

Si vous devez absolument passer une fonction dans la fonction car elle passe un argument supplémentaire ou l'appelle dans une boucle, vous pouvez plutôt passer un lambda:

public static string GetFormattedRate(
        Func<string> getRate)
{
    var rate = getRate();
    var formattedRate = rate.DollarsPerMonth.ToString("C0");
    return formattedRate;
}

var formattedRate = GetFormattedRate(()=>getRate(rateKey));

Le lambda liera les arguments que la fonction ne connaît pas et cachera qu'ils existent même.

monstre à cliquet
la source
-1

N'est-ce pas ce que tu veux?

class RateFormatter
{
    public abstract RateType GetRate(string rateKey);

    public abstract string FormatRate(RateType rate);

    public string GetFormattedRate(string rateKey)
    {
        var rate = GetRate(rateKey);
        var formattedRate = FormatRate(rate);
        return formattedRate;
    }
}

Et puis appelez-le comme ceci:

static class Program
{
    public static void Main()
    {
        var rateFormatter = new StandardRateFormatter(connectionStr);
        var formattedRate = rateFormatter.GetFormattedRate(rateKey);

        System.PrintLn(formattedRate);
    }
}

Si vous voulez une méthode qui peut se comporter de plusieurs manières différentes dans un langage orienté objet tel que C #, la façon habituelle de le faire est de faire en sorte que la méthode appelle une méthode abstraite. Si vous n'avez pas de raison spécifique de le faire d'une manière différente, vous devez le faire de cette façon.

Cela ressemble-t-il à une bonne solution ou y a-t-il des inconvénients auxquels vous pensez?

Tanner Swett
la source
1
Il y a quelques trucs bizarres dans votre réponse (pourquoi le formateur obtient-il également le taux, si c'est juste un formateur? Vous pouvez également supprimer la GetFormattedRateméthode et simplement appeler IRateFormatter.FormatRate(rate)). Le concept de base est cependant correct, et je pense que trop OP devrait implémenter son code polymorphiquement s'il a besoin de plusieurs méthodes de formatage.
BgrWorker