Jaka jest funkcjonalna alternatywa programowania dla interfejsu?

15

Jeśli chcę programować w stylu „funkcjonalnym”, czym zastąpiłbym interfejs?

interface IFace
{
   string Name { get; set; }
   int Id { get; }
}
class Foo : IFace { ... }

Może Tuple<>?

Tuple<Func<string> /*get_Name*/, Action<String> /*set_Name*/, Func<int> /*get_Id*/> Foo;

Jedynym powodem, dla którego używam interfejsu jest przede wszystkim to, że chcę, aby zawsze były dostępne pewne właściwości / metody.


Edycja: Więcej szczegółów na temat tego, co myślę / próbuję.

Powiedzmy, że mam metodę, która ma trzy funkcje:

static class Blarf
{
   public static void DoSomething(Func<string> getName, Action<string> setName, Func<int> getId);
}

Z instancją Barmogę użyć tej metody:

class Bar
{
   public string GetName();
   public void SetName(string value);

   public int GetId();
}
...
var bar = new Bar();
Blarf.DoSomething(bar.GetName, bar.SetName, bar.GetId);

Ale to trochę kłopotliwe, ponieważ muszę wspomnieć bartrzy razy w jednym telefonie. Ponadto, tak naprawdę nie zamierzam, aby osoby dzwoniące dostarczały funkcje z różnych instancji

Blarf.DoSomething(bar1.GetName, bar2.SetName, bar3.GetId); // NO!

W C # interfacejest jednym ze sposobów radzenia sobie z tym; ale to wydaje się być podejściem bardzo obiektowym. Zastanawiam się, czy istnieje bardziej funkcjonalne rozwiązanie: 1) przekaż grupę funkcji razem i 2) upewnij się, że funkcje są ze sobą odpowiednio powiązane.

.Аn
źródło
Nie zrobiłbyś tego. Interfejsy dla typów danych są całkowicie w porządku (choć wolisz obiekty niezmienne).
Telastyn
1
Rozdział 2 SICP prawie o tym mówi.
user16764
7
Po ponownym przeczytaniu pytania jestem ciekawy, jaką konkretną funkcjonalność próbujesz osiągnąć? Wygląda na to, że pytasz o to, jak zrobić program działający w stylu oo na stronie przeciwko instancji w funkcjonalnym stylu, co nie ma sensu.
Jimmy Hoffa
Odpowiedź byłaby zależna od języka. W Clojure możesz użyć clojure.org/protocols , gdzie jedynym miękkim obszarem są typy parametrów, na których funkcje muszą działać - są one obiektami - to wszystko, co wiesz.
Job
1
Uprość: struktura zawierająca te wskaźniki metod oraz funkcja inicjująca ją z instancji obiektu. Dlaczego Haskell? ;)
mlvljr

Odpowiedzi:

6

Nie traktuj programowania funkcjonalnego jako cienkiego forniru w stosunku do programowania imperatywnego; jest coś więcej niż tylko różnica składniowa.

W takim przypadku masz GetIDmetodę, która implikuje unikalność obiektów. To nie jest dobre podejście do pisania programów funkcjonalnych. Być może mógłbyś powiedzieć nam o problemie, który próbujesz rozwiązać, a my moglibyśmy udzielić Ci bardziej znaczących porad.

dan_waterworth
źródło
3
„Musisz myśleć po rosyjsku”.
4аn
Słusznie; mój prawdziwy problem nie jest szczególnie interesujący. Mam już wszystko działa dobrze w C # ( github.com/JDanielSmith/Projects/tree/master/PictureOfTheDay, jeśli naprawdę chcesz spojrzeć na kod), ale fajnie byłoby zrobić to w bardziej funkcjonalnym stylu, podczas gdy nadal używa C #.
2013 r. O
1
@ Isаn Czy to naprawdę cytat z Firefoksa (film) (bo to niesamowite, jeśli tak jest)? A może jest używany gdzie indziej?
icc97
2
Tam, gdzie zgadzam się, że jest to całkowita zmiana paradygmatu z programowania imperatywnego na programowanie funkcjonalne, istnieje wiele rzędów wielkości więcej linii kodu napisanych w sposób imperatywny. Dzięki imperialnemu programowaniu znaleziono o wiele więcej przypadków narożnych napisanych dla wielkich systemów. Istnieje wiele dobrych praktyk w programowaniu imperatywnym i warto wiedzieć, czy umiejętności te mogą zostać przetłumaczone, czy też nie stanowią problemu w PR. Całkowicie możliwe jest napisanie okropnego kodu w FP, więc tego rodzaju pytania powinny również podkreślać dobre strony FP.
icc97
11

Haskell i jego pochodne mają klasy, które są podobne do interfejsów. Choć brzmi to tak, jakbyś pytał o sposób enkapsulacji, to pytanie dotyczy systemów typów. System typów Hindley Milner jest powszechny w językach funkcjonalnych i ma typy danych, które robią to dla ciebie w różny sposób w różnych językach.

Jimmy Hoffa
źródło
5
+1 dla klas typów - główna różnica między typem klas Haskell a interfejsem Java polega na tym, że typ jest powiązany z typem po tym, jak oba są osobno zadeklarowane. Możesz użyć starego typu poprzez nowy „interfejs” tak łatwo, jak możesz użyć starego „interfejsu”, aby uzyskać dostęp do nowego typu. W przypadku ukrywania danych ukrywasz implementację typu w module. Przynajmniej według sławy Bertranda Meyera z Eiffla klasa OOP jest rodzajem modułu.
Steve314,
5

Istnieje kilka sposobów, aby umożliwić funkcji obsługę wielu wejść.

Pierwszy i najczęstszy: polimorfizm parametryczny.

Pozwala to funkcji działać na dowolne typy:

--Haskell Example
id :: a -> a --Here 'a' is just some arbitrary type
id myRandomThing = myRandomThing

head :: [a] -> a
head (listItem:list) = listItem

Co jest miłe, ale nie zapewnia dynamicznej wysyłki interfejsów OO. Do tego Haskell ma klasy, Scala ma implikacje itp

class Addable a where
   (<+>) :: a -> a -> a
instance Addable Int where
   a <+> b = a + b
instance Addable [a] where
   a <+> b = a ++ b

--Now we can get that do something similar to OO (kinda...)
addStuff :: (Addable a) => [a] -> a
-- Notice how we limit 'a' here to be something Addable
addStuff (x:[]) = x
addStuff (x:xs) = x <+> addStuff xs
-- In better Haskell form
addStuff' = foldl1 <+>

Pomiędzy tymi dwoma mechanizmami możesz wyrażać różnego rodzaju złożone i interesujące zachowania na swoich typach.

Daniel Gratzer
źródło
1
Możesz dodać wskazówki podświetlania sintax, gdy język w odpowiedzi nie pasuje do języka w pytaniu. Zobacz na przykład moją sugerowaną edycję.
hugomg
1

Podstawowa zasada jest taka, że ​​funkcje programowania FP wykonują tę samą pracę, co obiekty w programowaniu OO. Możesz wywoływać ich metody (cóż, w każdym razie metodę „call”), a one reagują zgodnie z niektórymi enkapsulowanymi wewnętrznymi regułami. W szczególności każdy przyzwoity język FP pozwala mieć w swojej funkcji „zmienne instancji” z zamknięciami / zasięgiem leksykalnym.

var make_OO_style_counter = function(){
   return {
      counter: 0
      increment: function(){
          this.counter += 1
          return this.counter;
      }
   }
};

var make_FP_style_counter = function(){
    var counter = 0;
    return fucntion(){
        counter += 1
        return counter;
    }
};

Teraz następne pytanie brzmi: co rozumiesz przez interfejs? Jednym z podejść jest używanie interfejsów nominalnych (jest zgodny z interfejsem, jeśli mówi, że tak robi) - ten zwykle zależy w dużej mierze od używanego języka, więc pozostawmy to na później. Innym sposobem zdefiniowania interfejsu jest sposób strukturalny, sprawdzający, jakie parametry odbiera i zwraca. Jest to rodzaj interfejsu, który zwykle widzisz w dynamicznych językach z kaczymi literami i bardzo dobrze pasuje do wszystkich FP: interfejs to tylko typy parametrów wejściowych do naszych funkcji i typy, które zwracają, więc wszystkie funkcje pasujące do poprawne typy pasują do interfejsu!

Dlatego najprostszym sposobem przedstawienia obiektu pasującego do interfejsu jest po prostu posiadanie grupy funkcji. Zwykle omija się brzydotę polegającą na przekazywaniu funkcji osobno, umieszczając je w jakimś rejestrze:

var my_blarfable = {
 get_name: function(){ ... },
 set_name: function(){ ... },
 get_id:   function(){ ... }
}

do_something(my_blarfable)

Używanie nagich funkcji lub zapisów funkcji znacznie rozwiąże większość typowych problemów w sposób „beztłuszczowy” bez ton płyty kotłowej. Jeśli potrzebujesz czegoś bardziej zaawansowanego, czasami języki oferują dodatkowe funkcje. Jednym z przykładów wspomnianych osób są klasy typu Haskell. Klasy typów zasadniczo kojarzą typ z jednym z tych rekordów funkcji i pozwalają pisać rzeczy, dzięki czemu słowniki są niejawne i automatycznie przenoszone do funkcji wewnętrznych, stosownie do przypadku.

-- Explicit dictionary version
-- no setters because haskell doesn't like mutable state.
data BlargDict = BlargDict {
    blarg_name :: String,
    blarg_id   :: Integer
}

do_something :: BlargDict -> IO()
do_something blarg_dict = do
   print (blarg_name blarg_dict)
   print (blarg_id   blarg_dict)

-- Typeclass version   
class Blargable a where
   blag_name :: a -> String
   blag_id   :: a -> String

do_something :: Blargable a => a -> IO
do_something blarg = do
   print (blarg_name blarg)
   print (blarg_id   blarg)

Jedną ważną rzeczą, na którą należy zwrócić uwagę w przypadku klas typów, jest to, że słowniki są powiązane z typami, a nie z wartościami (jak to, co dzieje się w słowniku i wersjach OO). Oznacza to, że system typów nie pozwala mieszać „typów” [1]. Jeśli potrzebujesz listy „blargabli” lub funkcji binarnej przestawiającej się na blargable, wówczas typy klas będą ograniczać wszystko do tego samego typu, podczas gdy podejście słownikowe pozwoli ci mieć blargable różnego pochodzenia (która wersja jest lepsza, zależy od tego, kim jesteś) robić)

[1] Istnieją zaawansowane sposoby robienia „typów egzystencjalnych”, ale zwykle nie są warte kłopotu.

hugomg
źródło
0

Myślę, że będzie to zależało od języka. Pochodzę z zawziętego pochodzenia. W wielu przypadkach interfejsy ze stanem do pewnego stopnia psują model funkcjonalny. Na przykład CLOS jest tam, gdzie LISP jest mniej funkcjonalny i bliższy imperatywnemu językowi. Zasadniczo wymagane parametry funkcji w połączeniu z metodami wyższego poziomu są prawdopodobnie tym, czego szukasz.

;; returns a function of the type #'(lambda (x y z &optional a b c)

(defun get-higher-level-method-impl (some-type-of-qualifier) 
    (cond ((eq 'foo) #'the-foo-version)
          ((eq 'bar) #'the-bar-version)))
Ipaul
źródło