Czy funkcje, które przyjmują funkcje jako parametry, również powinny przyjmować parametry do tych funkcji jako parametry?

20

Często zdarza mi się pisać funkcje, które wyglądają tak, ponieważ pozwalają mi łatwo kpić z dostępu do danych i nadal dostarczają podpis, który akceptuje parametry określające, do których danych mam dostęp.

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

Lub

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

Potem używam czegoś takiego:

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

Czy to powszechna praktyka? Czuję, że powinienem robić coś więcej

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

Ale to nie wydaje się działać zbyt dobrze, ponieważ musiałbym stworzyć nową funkcję, aby przejść do metody dla każdego rodzaju stawki.

Czasami czuję, że powinienem to robić

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

Ale wydaje się, że to zabiera ponowne pobieranie i formatowanie. Ilekroć chcę pobrać i sformatować, muszę napisać dwa wiersze, jeden do pobrania, a drugi do sformatowania.

Czego brakuje mi w programowaniu funkcjonalnym? Czy to właściwy sposób, aby to zrobić, czy jest lepszy wzór, który jest jednocześnie łatwy w utrzymaniu i użyciu?

rushinge
źródło
50
Rak DI rozprzestrzenił się do tej pory ...
Idan Arye
16
Z trudem widzę, dlaczego ta struktura miałaby być zastosowana. Z pewnością jest to wygodniejsze (i wyraźniej ) GetFormattedRate()jest zaakceptować szybkość do sformatowania jako parametru, w przeciwieństwie do akceptowania funkcji, która zwraca stawkę do sformatowania jako parametru?
aroth
6
Lepszym sposobem jest wykorzystanie closures gdzie przekazujesz sam parametr do funkcji, co w zamian daje ci funkcję odnoszącą się do tego konkretnego parametru. Ta „skonfigurowana” funkcja byłaby przekazywana jako parametr do funkcji, która jej używa.
Thomas Junk
7
@IdanArye DI rak?
Jules
11
Rak wstrzyknięcia uzależnienia @Jules
kat

Odpowiedzi:

39

Jeśli zrobisz to wystarczająco długo, w końcu będziesz musiał ciągle pisać tę funkcję:

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

Gratulacje, wymyśliłeś kompozycję funkcji .

Takie funkcje otoki nie mają większego zastosowania, gdy są wyspecjalizowane w jednym typie. Jeśli jednak wprowadzisz niektóre zmienne typu i pominiesz parametr wejściowy, twoja definicja GetFormattedRate wygląda następująco:

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

W tej chwili to, co robisz, ma niewielki cel. Nie jest ogólny, więc musisz powielić ten kod w dowolnym miejscu. Nadmiernie komplikuje twój kod, ponieważ teraz twój kod musi zebrać wszystko, czego potrzebuje, z tysiąca małych funkcji. Twoje serce jest jednak we właściwym miejscu: wystarczy przyzwyczaić się do używania tego rodzaju ogólnych funkcji wyższego rzędu, aby złożyć wszystko w całość. Lub skorzystać z dobrej starej mody lambda, aby włączyć Func<A, B>i Ado Func<B>.

Nie powtarzaj się.

Jacek
źródło
16
Powtórz to samo, jeśli uniknięcie powtórzenia się pogorszy kod. Tak jakbyś zawsze zapisywał te dwie linie zamiast FormatRate(GetRate(rateKey)).
user253751
6
@immibis Chyba chodzi o to, że GetFormattedRateod tej pory będzie mógł korzystać bezpośrednio.
Carles
Myślę, że to właśnie próbuję tutaj zrobić i próbowałem już wcześniej tej funkcji tworzenia, ale wydaje się, że rzadko ją używam, ponieważ moja druga funkcja często wymaga więcej niż jednego parametru. Być może muszę to zrobić w połączeniu z zamknięciami skonfigurowanych funkcji, o których wspomniał @ thomas-junk
rushinge
@rushinge Ten rodzaj kompozycji działa na typowej funkcji FP, która zawsze ma jeden argument (dodatkowe argumenty są tak naprawdę własnymi funkcjami, pomyśl o tym jak Func<Func<A, B>, C>); oznacza to, że potrzebujesz tylko jednej funkcji tworzenia, która działa dla dowolnej funkcji. Możesz jednak pracować z funkcjami C # wystarczająco dobrze, używając tylko zamknięć - zamiast przekazywać Func<rateKey, rateType>, naprawdę potrzebujesz Func<rateType>, a kiedy przekazujesz func, budujesz go tak () => GetRate(rateKey). Chodzi o to, że nie ujawniasz argumentów, o które funkcja docelowa nie dba.
Luaan,
1
@immibis Tak, Composefunkcja jest naprawdę przydatna tylko wtedy, gdy z GetRatejakiegoś powodu musisz opóźnić wykonanie , na przykład jeśli chcesz przejść Compose(FormatRate, GetRate)do funkcji, która zapewnia tempo według własnego wyboru, np. aby zastosować ją do każdego elementu w lista.
jpaugh
107

Nie ma absolutnie żadnego powodu, aby przekazywać funkcję i jej parametry, a jedynie wywoływać ją z tymi parametrami. W rzeczywistości, w przypadku, gdy nie ma powodu, aby przekazać funkcję w ogóle . Osoba wywołująca może równie dobrze wywołać samą funkcję i przekazać wynik.

Pomyśl o tym - zamiast używać:

var formattedRate = GetFormattedRate(getRate, rateType);

dlaczego nie użyć po prostu:

var formattedRate = GetFormattedRate(getRate(rateType));

?

Oprócz zmniejszenia niepotrzebnego kodu zmniejsza również sprzężenie - jeśli chcesz zmienić sposób pobierania stawki (powiedz, jeśli getRateteraz potrzebujesz dwóch argumentów), nie musisz zmieniaćGetFormattedRate .

Podobnie nie ma powodu, aby pisać GetFormattedRate(formatRate, getRate, rateKey)zamiast pisać formatRate(getRate(rateKey)).

Nie komplikuj rzeczy.

użytkownik253751
źródło
3
W takim przypadku masz rację. Ale jeśli funkcja wewnętrzna byłaby wywoływana wiele razy, powiedzmy w pętli lub funkcji mapy, przydatna byłaby zdolność przekazywania argumentów. Lub użyj kompozycji funkcjonalnej / curry, jak zaproponowano w odpowiedzi na @Jack.
user949300
15
@ user949300 może, ale nie jest to przypadek użycia OP (a jeśli tak, to może to formatRatebyć odwzorowane na stawkach, które należy sformatować).
jonrsharpe
4
@ user949300 Tylko wtedy, gdy twój język nie obsługuje zamykania lub gdy lamdy są ryzykowne do wpisania
Bergi
4
Zauważ, że przekazanie funkcji i jej parametrów do innej funkcji jest całkowicie poprawnym sposobem na opóźnienie oceny w języku bez leniwej semantyki en.wikipedia.org/wiki/Thunk
Jared Smith
4
@JaredSmith Przekazywanie funkcji, tak, przekazywanie jej parametrów, tylko jeśli twój język nie obsługuje zamknięć.
user253751
15

Jeśli absolutnie potrzebujesz przekazać funkcję do funkcji, ponieważ przekazuje ona dodatkowy argument lub wywołuje ją w pętli, możesz zamiast tego przekazać lambda:

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

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

Lambda powiąże argumenty, o których funkcja nie wie, i ukryje, że one nawet istnieją.

maniak zapadkowy
źródło
-1

Czy nie tego chcesz?

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

A potem nazwij to tak:

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

        System.PrintLn(formattedRate);
    }
}

Jeśli potrzebujesz metody, która może zachowywać się na wiele różnych sposobów w języku obiektowym, takim jak C #, zwykłym sposobem na to jest wywołanie metody abstrakcyjnej. Jeśli nie masz konkretnego powodu, aby zrobić to inaczej, powinieneś to zrobić w ten sposób.

Czy to wygląda na dobre rozwiązanie, czy może są jakieś wady?

Tanner Swett
źródło
1
W twojej odpowiedzi jest kilka dziwnych rzeczy (dlaczego formatyzator również otrzymuje stawkę, jeśli to tylko formatyzator? Możesz także usunąć GetFormattedRatemetodę i po prostu wywołać IRateFormatter.FormatRate(rate)). Podstawowa koncepcja jest jednak poprawna i myślę, że również OP powinien zaimplementować polimorficznie swój kod, jeśli potrzebuje wielu metod formatowania.
BgrWorker