¿Deberían las funciones que toman funciones como parámetros, también llevar parámetros a esas funciones como parámetros?

20

A menudo me encuentro escribiendo funciones que se ven así porque me permiten burlarme fácilmente del acceso a los datos y aún así proporcionar una firma que acepta parámetros para determinar a qué datos acceder.

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

O

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

Entonces lo uso algo como esto:

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);
}

¿Es esta una práctica común? Siento que debería estar haciendo algo más como

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

Pero eso no parece funcionar muy bien porque tendría que hacer una nueva función para pasar al método para cada tipo de tasa.

A veces siento que debería estar haciendo

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

Pero eso parece eliminar cualquier reutilización de búsqueda y formato. Cada vez que quiero buscar y formatear tengo que escribir dos líneas, una para buscar y otra para formatear.

¿Qué me falta de programación funcional? ¿Es esta la forma correcta de hacerlo, o hay un patrón mejor que sea fácil de mantener y usar?

rushinge
fuente
50
El cáncer DI se ha extendido hasta ahora ...
Idan Arye
16
Me cuesta ver por qué esta estructura se usaría en primer lugar. ¿Seguramente es más conveniente (y claro ) GetFormattedRate()aceptar la velocidad para formatear como parámetro, en lugar de que acepte una función que devuelve la velocidad para formatear como parámetro?
Aroth
66
Una mejor manera es utilizar el lugar closuresdonde pasa el parámetro a una función, que a cambio le da una función que se refiere a ese parámetro específico. Esta función "configurada" se pasaría como un parámetro a la función, que la utiliza.
Thomas Junk
77
@IdanArye DI cáncer?
Jules
11
Cáncer de inyección de dependencia de @Jules
gato

Respuestas:

39

Si hace esto lo suficiente, eventualmente se encontrará escribiendo esta función una y otra vez:

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

Felicidades, has inventado la composición de funciones .

Las funciones de envoltura como esta no tienen mucho uso cuando están especializadas en un tipo. Sin embargo, si introduce algunas variables de tipo y omite el parámetro de entrada, su definición de GetFormattedRate se verá así:

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);

Tal como está, lo que estás haciendo tiene poco propósito. No es genérico, por lo que debe duplicar ese código por todas partes. Sobrecomplica su código porque ahora su código tiene que ensamblar todo lo que necesita de mil funciones pequeñas por sí solo. Sin embargo, su corazón está en el lugar correcto: solo necesita acostumbrarse a usar este tipo de funciones genéricas de orden superior para armar las cosas. O bien, utilizar una buena lambda vieja manera de convertir Func<A, B>y Aen Func<B>.

No te repitas a ti mismo.

Jack
fuente
16
Repítete si evitar repetirlo empeora el código. Como si siempre escribieras esas dos líneas en lugar de FormatRate(GetRate(rateKey)).
user253751
66
@immibis Supongo que la idea es que podrá usarlo GetFormattedRatedirectamente de ahora en adelante.
Carles
Creo que esto es lo que estoy tratando de hacer aquí, y he probado esta función de composición antes, pero parece que rara vez puedo usarla porque mi segunda función a menudo necesita más de un parámetro. Quizás necesito hacer esto en combinación con cierres para funciones configuradas como lo menciona @ thomas-junk
rushinge
@rushinge Este tipo de composición funciona en la función típica de FP que siempre tiene un único argumento (los argumentos adicionales son realmente funciones propias, piense en ello Func<Func<A, B>, C>); Esto significa que solo necesita una función de composición que funcione para cualquier función. Sin embargo, puede trabajar con las funciones de C # lo suficientemente bien con solo usar cierres; en lugar de pasar Func<rateKey, rateType>, realmente solo necesita Func<rateType>, y al pasar el func, lo construye como () => GetRate(rateKey). El punto es que no expones los argumentos que a la función objetivo no le importan.
Luaan
1
@immibis Sí, la Composefunción en realidad solo es útil si necesita retrasar la ejecución de GetRatepor alguna razón, como si desea pasar Compose(FormatRate, GetRate)a una función que proporciona una tasa de su propia elección, por ejemplo, para aplicarla a cada elemento en un lista.
jpaugh
107

No hay absolutamente ninguna razón para pasar una función y sus parámetros, solo para luego llamarla con esos parámetros. De hecho, en su caso no tiene ninguna razón para pasar una función en absoluto . La persona que llama también podría llamar a la función en sí y pasar el resultado.

Piénselo, en lugar de usar:

var formattedRate = GetFormattedRate(getRate, rateType);

por qué no simplemente usar:

var formattedRate = GetFormattedRate(getRate(rateType));

?

Además de reducir el código innecesario, también reduce el acoplamiento: si desea cambiar la forma en que se obtiene la velocidad (por ejemplo, si getRateahora necesita dos argumentos), no tiene que cambiar GetFormattedRate.

Del mismo modo, no hay razón para escribir en GetFormattedRate(formatRate, getRate, rateKey)lugar de escribir formatRate(getRate(rateKey)).

No compliques demasiado las cosas.

usuario253751
fuente
3
En este caso tienes razón. Pero si la función interna se llamara varias veces, digamos en un bucle o una función de mapa, entonces la capacidad de pasar argumentos sería útil. O use la composición funcional / curry como se propone en @Jack answer.
user949300
15
@ user949300 tal vez, pero ese no es el caso de uso del OP (y si lo fuera, tal vez sea formatRateeso lo que debería asignarse a las tasas que deberían formatearse).
jonrsharpe
44
@ user949300 Solo si su idioma no admite cierres o cuando las lamdas son un obstáculo para escribir
Bergi
44
Tenga en cuenta que pasar una función y sus parámetros a otra función es una forma totalmente válida de retrasar la evaluación en un lenguaje sin semántica perezosa es.wikipedia.org/wiki/Thunk
Jared Smith
44
@JaredSmith Pasando la función, sí, pasando sus parámetros, solo si su idioma no admite cierres.
user253751
15

Si absolutamente necesita pasar una función a la función porque pasa algún argumento adicional o la llama en un bucle, entonces puede pasar una lambda:

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

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

La lambda vinculará los argumentos que la función no conoce y ocultará que incluso existen.

monstruo de trinquete
fuente
-1

¿No es esto lo que quieres?

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;
    }
}

Y luego llámalo así:

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

        System.PrintLn(formattedRate);
    }
}

Si desea un método que pueda comportarse de múltiples maneras diferentes en un lenguaje orientado a objetos como C #, la forma habitual de hacerlo es hacer que el método llame a un método abstracto. Si no tiene una razón específica para hacerlo de una manera diferente, debe hacerlo de esa manera.

¿Esto parece una buena solución, o hay inconvenientes en los que estás pensando?

Tanner Swett
fuente
1
Hay un par de cosas extrañas en su respuesta (¿por qué el formateador también obtiene la tasa, si es solo un formateador? También puede eliminar el GetFormattedRatemétodo y simplemente llamar IRateFormatter.FormatRate(rate)). Sin embargo, el concepto básico es correcto, y creo que también OP debería implementar su código polimórficamente si necesita múltiples métodos de formateo.
BgrWorker