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 #