Próbuję zaprojektować bibliotekę w języku F #. Biblioteka powinna być przyjazna do użytku zarówno z języka F #, jak i C # .
I tutaj trochę utknąłem. Mogę uczynić go przyjaznym dla języka F # lub mogę uczynić go przyjaznym dla C #, ale problem polega na tym, jak uczynić go przyjaznym dla obu.
Oto przykład. Wyobraź sobie, że mam następującą funkcję w F #:
let compose (f: 'T -> 'TResult) (a : 'TResult -> unit) = f >> a
Jest to doskonale użyteczne z F #:
let useComposeInFsharp() =
let composite = compose (fun item -> item.ToString) (fun item -> printfn "%A" item)
composite "foo"
composite "bar"
W języku C # compose
funkcja ma następujący podpis:
FSharpFunc<T, Unit> compose<T, TResult>(FSharpFunc<T, TResult> f, FSharpFunc<TResult, Unit> a);
Ale oczywiście nie chcę FSharpFunc
w podpisie, to czego chcę, Func
a Action
zamiast tego tak:
Action<T> compose2<T, TResult>(Func<T, TResult> f, Action<TResult> a);
Aby to osiągnąć, mogę stworzyć compose2
taką funkcję:
let compose2 (f: Func<'T, 'TResult>) (a : Action<'TResult> ) =
new Action<'T>(f.Invoke >> a.Invoke)
Teraz jest to doskonale użyteczne w C #:
void UseCompose2FromCs()
{
compose2((string s) => s.ToUpper(), Console.WriteLine);
}
Ale teraz mamy problem compose2
z używaniem z F #! Teraz muszę zawinąć wszystkie standardowe F # funs
w Func
i Action
tak:
let useCompose2InFsharp() =
let f = Func<_,_>(fun item -> item.ToString())
let a = Action<_>(fun item -> printfn "%A" item)
let composite2 = compose2 f a
composite2.Invoke "foo"
composite2.Invoke "bar"
Pytanie: jak możemy osiągnąć pierwszorzędne wrażenia z obsługi biblioteki napisanej w języku F # zarówno dla użytkowników języka F #, jak i C #?
Jak dotąd nie mogłem wymyślić nic lepszego niż te dwa podejścia:
- Dwa oddzielne zestawy: jeden przeznaczony dla użytkowników języka F #, a drugi dla użytkowników języka C #.
- Jeden zestaw, ale różne przestrzenie nazw: jedna dla użytkowników języka F #, a druga dla użytkowników języka C #.
W przypadku pierwszego podejścia zrobiłbym coś takiego:
Utwórz projekt F #, nazwij go FooBarFs i skompiluj do FooBarFs.dll.
- Skieruj bibliotekę wyłącznie na użytkowników języka F #.
- Ukryj wszystko, co jest niepotrzebne w plikach .fsi.
Utwórz inny projekt F #, wywołaj if FooBarCs i skompiluj go do FooFar.dll
- Ponownie użyj pierwszego projektu F # na poziomie źródła.
- Utwórz plik .fsi, który ukrywa wszystko z tego projektu.
- Utwórz plik .fsi, który uwidacznia bibliotekę w C # sposób, używając idiomów C # dla nazw, przestrzeni nazw itp.
- Utwórz opakowania, które delegują do biblioteki podstawowej, wykonując konwersję w razie potrzeby.
Myślę, że drugie podejście z przestrzeniami nazw może być mylące dla użytkowników, ale wtedy masz jeden zespół.
Pytanie: żadne z nich nie jest idealne, być może brakuje mi jakiejś flagi / przełącznika / atrybutu kompilatora lub jakiejś sztuczki, a jest na to lepszy sposób?
Pytanie: czy ktoś inny próbował osiągnąć coś podobnego, a jeśli tak, to jak to zrobiłeś?
EDYCJA: aby wyjaśnić, pytanie dotyczy nie tylko funkcji i delegatów, ale także ogólnego doświadczenia użytkownika C # z biblioteką F #. Obejmuje to przestrzenie nazw, konwencje nazewnictwa, idiomy i tym podobne, które są natywne dla języka C #. Zasadniczo użytkownik C # nie powinien być w stanie wykryć, że biblioteka została utworzona w języku F #. I odwrotnie, użytkownik języka F # powinien mieć ochotę mieć do czynienia z biblioteką C #.
EDYCJA 2:
Z dotychczasowych odpowiedzi i komentarzy widzę, że moje pytanie nie jest wystarczająco szczegółowe, być może głównie z powodu użycia tylko jednego przykładu, w którym pojawiają się problemy z interoperacyjnością między F # i C #, kwestią wartości funkcji. Myślę, że jest to najbardziej oczywisty przykład, więc to skłoniło mnie do zadawania tego pytania, ale tym samym sprawiło wrażenie, że jest to jedyny problem, którym się zajmuję.
Podam bardziej konkretne przykłady. Przeczytałem najbardziej doskonały dokument Wytyczne dotyczące projektowania komponentów F # (wielkie dzięki @gradbot za to!). Wytyczne zawarte w dokumencie, jeśli są stosowane, dotyczą niektórych problemów, ale nie wszystkich.
Dokument jest podzielony na dwie główne części: 1) wytyczne dotyczące kierowania na użytkowników języka F #; oraz 2) wskazówki dotyczące kierowania na użytkowników języka C #. Nigdzie nawet nie próbuje udawać, że możliwe jest jednolite podejście, co dokładnie odpowiada mojemu pytaniu: możemy kierować na język F #, możemy kierować na C #, ale jakie jest praktyczne rozwiązanie dla obu?
Przypominamy, że celem jest posiadanie biblioteki utworzonej w języku F #, która może być używana idiomatycznie z języków F # i C #.
Słowo kluczowe jest tutaj idiomatyczne . Problemem nie jest ogólna interoperacyjność, w przypadku której możliwe jest po prostu korzystanie z bibliotek w różnych językach.
Przejdźmy teraz do przykładów, które biorę bezpośrednio z wytycznych dotyczących projektowania komponentów języka F # .
Moduły + funkcje (F #) a przestrzenie nazw + typy + funkcje
F #: używaj przestrzeni nazw lub modułów do przechowywania typów i modułów. Idiomatyczne zastosowanie to umieszczanie funkcji w modułach, np .:
// library module Foo let bar() = ... let zoo() = ... // Use from F# open Foo bar() zoo()
C #: Używaj przestrzeni nazw, typów i członków jako podstawowej struktury organizacyjnej dla swoich komponentów (w przeciwieństwie do modułów), dla podstawowych interfejsów API .NET.
Jest to niezgodne z wytycznymi języka F #, a przykład musiałby zostać ponownie napisany, aby pasował do użytkowników C #:
[<AbstractClass; Sealed>] type Foo = static member bar() = ... static member zoo() = ...
Robiąc to jednak, przerywamy idiomatyczne użycie z F #, ponieważ nie możemy go już używać
bar
izoo
bez przedrostkaFoo
.
Korzystanie z krotek
F #: używaj krotek, gdy jest to odpowiednie dla zwracanych wartości.
C #: Unikaj używania krotek jako wartości zwracanych w podstawowych interfejsach API platformy .NET.
Async
F #: używaj Async do programowania asynchronicznego na granicach interfejsu API F #.
C #: udostępniaj operacje asynchroniczne przy użyciu asynchronicznego modelu programowania platformy .NET (BeginFoo, EndFoo) lub jako metody zwracające zadania platformy .NET (Task), a nie jako obiekty F # Async.
Zastosowanie
Option
F #: Rozważ użycie wartości opcji dla typów zwracanych zamiast zgłaszania wyjątków (dla kodu F # -facing).
Rozważ użycie wzorca TryGetValue zamiast zwracania wartości opcji F # (opcja) w podstawowych interfejsach API platformy .NET i preferuj przeciążanie metod zamiast przyjmowania wartości opcji F # jako argumentów.
Związki dyskryminowane
F #: używaj związków rozłącznych jako alternatywy dla hierarchii klas do tworzenia danych o strukturze drzewa
C #: brak konkretnych wytycznych w tym zakresie, ale pojęcie związków dyskryminowanych jest obce dla języka C #
Funkcje curry
F #: funkcje curried są idiomatyczne dla F #
C #: nie używaj currying parametrów w podstawowych interfejsach API .NET.
Sprawdzanie wartości null
F #: to nie jest idiomatyczne dla F #
C #: rozważ sprawdzenie wartości null w podstawowych granicach interfejsu API platformy .NET.
Korzystanie z F # typów
list
,map
,set
, etcF #: idiomatyczne jest używanie ich w F #
C #: Rozważ użycie IEnumerable i IDictionary typów interfejsów kolekcji .NET dla parametrów i wartości zwracanych w podstawowych interfejsach API platformy .NET. ( Czyli nie używać F #
list
,map
,set
)
Typy funkcji (oczywiste)
F #: użycie funkcji F # jako wartości jest oczywiście idiomatyczne dla F #
C #: używaj typów delegatów .NET zamiast typów funkcji F # w podstawowych interfejsach API .NET.
Myślę, że to powinno wystarczyć do zademonstrowania natury mojego pytania.
Nawiasem mówiąc, wytyczne zawierają również częściową odpowiedź:
... typową strategią implementacji przy opracowywaniu metod wyższego rzędu dla podstawowych bibliotek .NET jest stworzenie całej implementacji przy użyciu typów funkcji F #, a następnie utworzenie publicznego interfejsu API przy użyciu delegatów jako cienkiej fasady na szczycie rzeczywistej implementacji języka F #.
Podsumować.
Jest jedna ostateczna odpowiedź: nie ma żadnych sztuczek kompilatora, których nie przegapiłem .
Zgodnie z dokumentem z wytycznymi wydaje się, że rozsądną strategią jest najpierw napisanie dla języka F #, a następnie utworzenie opakowania fasady dla platformy .NET.
Pozostaje zatem pytanie dotyczące praktycznej realizacji tego:
Oddzielne zespoły? lub
Różne przestrzenie nazw?
Jeśli moja interpretacja jest poprawna, Tomas sugeruje, że użycie oddzielnych przestrzeni nazw powinno być wystarczające i powinno być akceptowalnym rozwiązaniem.
Myślę, że zgodzę się z tym, biorąc pod uwagę, że wybór przestrzeni nazw jest taki, że nie zaskakuje ani nie myli użytkowników .NET / C #, co oznacza, że przestrzeń nazw dla nich powinna prawdopodobnie wyglądać tak, jakby była dla nich podstawową przestrzenią nazw. Użytkownicy języka F # będą musieli wziąć na siebie ciężar wybierania przestrzeni nazw specyficznej dla języka F #. Na przykład:
FSharp.Foo.Bar -> przestrzeń nazw dla języka F # zwróconego w stronę biblioteki
Foo.Bar -> przestrzeń nazw dla otoki .NET, idiomatyczna dla C #
Odpowiedzi:
Daniel wyjaśnił już, jak zdefiniować napisaną przez siebie funkcję F # przyjazną dla języka C #, więc dodam kilka komentarzy wyższego poziomu. Przede wszystkim powinieneś przeczytać Wytyczne dotyczące projektowania komponentów F # (do których odwołuje się już gradbot). Jest to dokument, w którym wyjaśniono, jak projektować biblioteki F # i .NET przy użyciu języka F # i powinien on zawierać odpowiedzi na wiele pytań.
Korzystając z języka F #, można pisać zasadniczo dwa rodzaje bibliotek:
Biblioteka F # jest przeznaczona do użytku tylko z F #, więc jej interfejs publiczny jest napisany w stylu funkcjonalnym (przy użyciu typów funkcji F #, krotek, związków rozłącznych itp.)
Biblioteka .NET jest przeznaczona do użytku z dowolnym językiem .NET (w tym C # i F #) i zazwyczaj jest zgodna ze stylem obiektowym .NET. Oznacza to, że będziesz udostępniać większość funkcji jako klasy z metodą (a czasami metody rozszerzające lub metody statyczne, ale głównie kod powinien być napisany w projekcie obiektowym).
W swoim pytaniu pytasz, jak wyeksponować kompozycję funkcji jako bibliotekę .NET, ale myślę, że funkcje takie jak twoja
compose
są koncepcjami zbyt niskiego poziomu z punktu widzenia biblioteki .NET. Możesz pokazać je jako metody współpracujące zFunc
iAction
, ale prawdopodobnie nie w taki sposób zaprojektowałbyś normalną bibliotekę .NET w pierwszej kolejności (być może zamiast tego użyłbyś wzorca Builder lub czegoś podobnego).W niektórych przypadkach (np. Podczas projektowania bibliotek numerycznych, które tak naprawdę nie pasują do stylu biblioteki .NET), dobrze jest zaprojektować bibliotekę, która łączy style F # i .NET w jednej bibliotece. Najlepszym sposobem na to jest posiadanie normalnego interfejsu API F # (lub normalnego .NET), a następnie udostępnienie opakowań do naturalnego użytku w innym stylu. Opakowania mogą znajdować się w oddzielnej przestrzeni nazw (jak
MyLibrary.FSharp
iMyLibrary
).W naszym przykładzie możesz pozostawić implementację F # w,
MyLibrary.FSharp
a następnie dodać opakowania .NET (przyjazne dla C #) (podobne do kodu, który opublikował Daniel) wMyLibrary
przestrzeni nazw jako metodę statyczną jakiejś klasy. Ale znowu, biblioteka .NET prawdopodobnie miałaby bardziej specyficzne API niż kompozycja funkcji.źródło
Wystarczy zawinąć wartości funkcji ( funkcje częściowo zastosowane itp.) Za pomocą
Func
lubAction
, pozostałe są konwertowane automatycznie. Na przykład:Dlatego sensowne jest użycie
Func
/ wAction
stosownych przypadkach. Czy to eliminuje Twoje obawy? Uważam, że proponowane przez Ciebie rozwiązania są zbyt skomplikowane. Możesz napisać całą swoją bibliotekę w F # i korzystać z niej bezboleśnie z poziomu F # i C # (robię to cały czas).Ponadto język F # jest bardziej elastyczny niż C # pod względem współdziałania, więc ogólnie najlepiej jest postępować zgodnie z tradycyjnym stylem .NET, gdy jest to istotne.
EDYTOWAĆ
Wydaje mi się, że praca wymagana do utworzenia dwóch publicznych interfejsów w oddzielnych przestrzeniach nazw jest uzasadniona tylko wtedy, gdy są one komplementarne lub funkcja F # nie jest użyteczna z języka C # (na przykład funkcje wbudowane, które zależą od metadanych specyficznych dla języka F #).
Zbierając punkty po kolei:
Moduł +
let
powiązania i typ bez konstruktora + statyczne elementy członkowskie wyglądają dokładnie tak samo w C #, więc jeśli możesz, korzystaj z modułów. Możesz użyć,CompiledNameAttribute
aby nadać członkom przyjazne dla języka C # nazwy.Może się mylę, ale przypuszczam, że wytyczne dotyczące składników zostały napisane przed
System.Tuple
dodaniem do frameworka. (We wcześniejszych wersjach F # zdefiniował swój własny typ krotki). Od tego czasu bardziej akceptowalne stało się użycieTuple
w interfejsie publicznym dla trywialnych typów.Myślę, że w tym miejscu musisz robić rzeczy w C # sposób, ponieważ F
Task
# działa dobrze, ale C # nie działa dobrzeAsync
. Możesz użyć async wewnętrznie, a następnie wywołaćAsync.StartAsTask
przed powrotem z metody publicznej.Objęcie
null
może być największą pojedynczą wadą podczas tworzenia interfejsu API do użytku z języka C #. W przeszłości próbowałem różnych sztuczek, aby uniknąć uwzględniania wartości null w wewnętrznym kodzie F #, ale ostatecznie najlepiej było oznaczać typy za pomocą publicznych konstruktorów[<AllowNullLiteral>]
i sprawdzać argumenty pod kątem wartości null. Pod tym względem nie jest gorszy niż C #.Związki rozłączne są zwykle kompilowane do hierarchii klas, ale zawsze mają względnie przyjazną reprezentację w języku C #. Powiedziałbym, oznaczyć je
[<AllowNullLiteral>]
i używać.Funkcje curried generują wartości funkcji , których nie należy używać.
Okazało się, że lepiej jest objąć null niż polegać na tym, że zostanie przechwycony przez interfejs publiczny i zignorować go wewnętrznie. YMMV.
Używanie
list
/map
/set
wewnętrznie ma sens . Wszystkie mogą być ujawniane za pośrednictwem interfejsu publicznego jakoIEnumerable<_>
. Równieżseq
,dict
iSeq.readonly
często są użyteczne.Zobacz # 6.
Wybór strategii zależy od typu i rozmiaru biblioteki, ale z mojego doświadczenia wynika, że znalezienie idealnego punktu między językami F # i C # wymagało mniej pracy - na dłuższą metę - niż tworzenie oddzielnych interfejsów API.
źródło
module Foo.Bar.Zoo
w C # będzie toclass Foo
w przestrzeni nazwFoo.Bar
. Jeśli jednak masz zagnieżdżone moduły, takie jakmodule Foo=... module Bar=... module Zoo==
, spowoduje to wygenerowanie klasyFoo
z klasami wewnętrznymiBar
iZoo
. ReIEnumerable
, to może być nie do zaakceptowania, gdy naprawdę musimy pracować z mapami i zestawami. Renull
wartości iAllowNullLiteral
- jednym z powodów, dla których lubię F # jest to, że jestem zmęczony językiemnull
C #, naprawdę nie chciałbym ponownie zanieczyszczać tego wszystkiego!Chociaż prawdopodobnie byłaby to przesada, możesz rozważyć napisanie aplikacji wykorzystującej Mono.Cecil (ma świetne wsparcie na liście mailingowej), która zautomatyzowałaby konwersję na poziomie IL. Na przykład zaimplementujesz zestaw w języku F # przy użyciu publicznego interfejsu API w stylu F #, a następnie narzędzie wygeneruje otokę przyjazną dla języka C #.
Na przykład w języku F # oczywiście używałbyś
option<'T>
(None
konkretnie) zamiast używanianull
jak w C #. Napisanie generatora opakowania dla tego scenariusza powinno być dość łatwe: metoda opakowująca wywołałaby oryginalną metodę: jeśli zwracana była wartośćSome x
, to returnx
, w przeciwnym razie returnnull
.Będziesz musiał obsłużyć przypadek, gdy
T
jest typem wartości, tj. Nie dopuszcza wartości null; musiałbyś zawijać wartość zwracaną przez metodę opakowującąNullable<T>
, co sprawia, że jest to trochę bolesne.Ponownie, jestem pewien, że warto byłoby napisać takie narzędzie w swoim scenariuszu, być może z wyjątkiem sytuacji, gdy będziesz regularnie pracować nad tą biblioteką (bezproblemowo używaną z F # i C #). W każdym razie myślę, że byłby to interesujący eksperyment, który może nawet kiedyś zbadam.
źródło
Mono.Cecil
oznaczałoby w zasadzie napisanie programu do ładowania zestawu F #, znajdowania elementów wymagających mapowania i emitowania przez nie innego zestawu z opakowaniami C #? Czy dobrze to zrobiłem?Wersja robocza wytycznych dotyczących projektowania składników języka F # (sierpień 2010)
źródło