Tôi đang cố gắng thiết kế một thư viện trong F #. Thư viện phải thân thiện để sử dụng từ cả F # và C # .
Và đây là nơi tôi bị mắc kẹt một chút. Tôi có thể làm cho nó thân thiện với F #, hoặc tôi có thể làm cho nó thân thiện với C #, nhưng vấn đề là làm sao để nó thân thiện với cả hai.
Đây là một ví dụ. Hãy tưởng tượng tôi có hàm sau trong F #:
let compose (f: 'T -> 'TResult) (a : 'TResult -> unit) = f >> a
Điều này hoàn toàn có thể sử dụng được từ F #:
let useComposeInFsharp() =
let composite = compose (fun item -> item.ToString) (fun item -> printfn "%A" item)
composite "foo"
composite "bar"
Trong C #, compose
hàm có chữ ký sau:
FSharpFunc<T, Unit> compose<T, TResult>(FSharpFunc<T, TResult> f, FSharpFunc<TResult, Unit> a);
Nhưng tất nhiên, tôi không muốn có FSharpFunc
chữ ký, những gì tôi muốn là Func
và Action
thay vào đó, như thế này:
Action<T> compose2<T, TResult>(Func<T, TResult> f, Action<TResult> a);
Để đạt được điều này, tôi có thể tạo compose2
hàm như sau:
let compose2 (f: Func<'T, 'TResult>) (a : Action<'TResult> ) =
new Action<'T>(f.Invoke >> a.Invoke)
Bây giờ, điều này hoàn toàn có thể sử dụng được trong C #:
void UseCompose2FromCs()
{
compose2((string s) => s.ToUpper(), Console.WriteLine);
}
Nhưng bây giờ chúng ta gặp sự cố khi sử dụng compose2
từ F #! Bây giờ tôi phải gói tất cả F # tiêu chuẩn funs
vào Func
và Action
, như thế này:
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"
Câu hỏi: Làm thế nào chúng ta có thể đạt được trải nghiệm hạng nhất cho thư viện được viết bằng F # cho cả người dùng F # và C #?
Cho đến nay, tôi không thể nghĩ ra điều gì tốt hơn hai cách tiếp cận sau:
- Hai tập hợp riêng biệt: một tập hợp nhắm mục tiêu đến người dùng F # và tập hợp thứ hai nhắm mục tiêu đến người dùng C #.
- Một tập hợp nhưng không gian tên khác nhau: một cho người dùng F # và một cho người dùng C #.
Đối với cách tiếp cận đầu tiên, tôi sẽ làm như sau:
Tạo một dự án F #, gọi nó là FooBarFs và biên dịch nó thành FooBarFs.dll.
- Nhắm mục tiêu thư viện hoàn toàn đến người dùng F #.
- Ẩn mọi thứ không cần thiết khỏi tệp .fsi.
Tạo một dự án F # khác, gọi if FooBarCs và biên dịch nó thành FooFar.dll
- Sử dụng lại dự án F # đầu tiên ở cấp nguồn.
- Tạo tệp .fsi ẩn mọi thứ khỏi dự án đó.
- Tạo tệp .fsi hiển thị thư viện theo cách C #, sử dụng các thành ngữ C # cho tên, không gian tên, v.v.
- Tạo các trình bao bọc ủy quyền cho thư viện lõi, thực hiện chuyển đổi khi cần thiết.
Tôi nghĩ rằng cách tiếp cận thứ hai với không gian tên có thể gây nhầm lẫn cho người dùng, nhưng sau đó bạn có một tập hợp.
Câu hỏi: Không có cái nào trong số này là lý tưởng, có lẽ tôi đang thiếu một số loại cờ / chuyển đổi / thuộc tính của trình biên dịch hoặc một số loại thủ thuật và có một cách tốt hơn để làm điều này?
Câu hỏi: đã có ai khác cố gắng đạt được điều gì đó tương tự chưa và nếu có thì bạn đã làm như thế nào?
CHỈNH SỬA: để làm rõ, câu hỏi không chỉ về các hàm và đại biểu mà còn là trải nghiệm tổng thể của người dùng C # với thư viện F #. Điều này bao gồm không gian tên, quy ước đặt tên, thành ngữ và những thứ tương tự như vậy có nguồn gốc từ C #. Về cơ bản, người dùng C # sẽ không thể phát hiện ra rằng thư viện được tạo ra trong F #. Và ngược lại, người dùng F # sẽ cảm thấy thích giao dịch với thư viện C #.
CHỈNH SỬA 2:
Tôi có thể thấy từ các câu trả lời và nhận xét cho đến nay rằng câu hỏi của tôi thiếu độ sâu cần thiết, có lẽ chủ yếu là do chỉ sử dụng một ví dụ trong đó các vấn đề về khả năng tương tác giữa F # và C # phát sinh, vấn đề về giá trị hàm. Tôi nghĩ đây là ví dụ rõ ràng nhất và vì vậy điều này đã khiến tôi sử dụng nó để đặt câu hỏi, nhưng cùng một dấu hiệu đã tạo ấn tượng rằng đây là vấn đề duy nhất tôi quan tâm.
Hãy để tôi cung cấp các ví dụ cụ thể hơn. Tôi đã đọc qua tài liệu Hướng dẫn thiết kế thành phần F # tuyệt vời nhất (rất cám ơn @gradbot về điều này!). Các hướng dẫn trong tài liệu, nếu được sử dụng, sẽ giải quyết một số vấn đề nhưng không phải tất cả.
Tài liệu được chia thành hai phần chính: 1) hướng dẫn nhắm mục tiêu người dùng F #; và 2) hướng dẫn nhắm mục tiêu người dùng C #. Không nơi nào nó thậm chí cố gắng giả vờ rằng có thể có một cách tiếp cận thống nhất, điều này lặp lại chính xác câu hỏi của tôi: chúng ta có thể nhắm mục tiêu F #, chúng ta có thể nhắm mục tiêu C #, nhưng giải pháp thực tế để nhắm mục tiêu cả hai là gì?
Để nhắc nhở, mục tiêu là có một thư viện được tạo ra bằng F # và có thể được sử dụng thành ngữ từ cả hai ngôn ngữ F # và C #.
Từ khóa ở đây là thành ngữ . Vấn đề không phải là khả năng tương tác chung mà chỉ có thể sử dụng các thư viện bằng các ngôn ngữ khác nhau.
Bây giờ đến các ví dụ, mà tôi lấy ngay từ Hướng dẫn thiết kế thành phần F # .
Mô-đun + chức năng (F #) so với Không gian tên + Loại + chức năng
F #: Sử dụng không gian tên hoặc mô-đun để chứa các loại và mô-đun của bạn. Cách sử dụng thành ngữ là đặt các chức năng trong các mô-đun, ví dụ:
// library module Foo let bar() = ... let zoo() = ... // Use from F# open Foo bar() zoo()
C #: Sử dụng không gian tên, kiểu và thành viên làm cấu trúc tổ chức chính cho các thành phần của bạn (trái ngược với mô-đun), cho các API .NET vani.
Điều này không tương thích với hướng dẫn F # và ví dụ sẽ cần được viết lại để phù hợp với người dùng C #:
[<AbstractClass; Sealed>] type Foo = static member bar() = ... static member zoo() = ...
Mặc dù vậy, bằng cách làm như vậy, chúng ta phá vỡ cách sử dụng thành ngữ từ F # vì chúng ta không thể sử dụng nữa
bar
vàzoo
không có tiền tốFoo
.
Sử dụng bộ giá trị
F #: Sử dụng bộ giá trị khi thích hợp cho các giá trị trả về.
C #: Tránh sử dụng các bộ giá trị làm giá trị trả về trong các API .NET vani.
Không đồng bộ
F #: Sử dụng Async để lập trình không đồng bộ ở các ranh giới API F #.
C #: Hiển thị các hoạt động không đồng bộ bằng cách sử dụng mô hình lập trình không đồng bộ .NET (BeginFoo, EndFoo) hoặc dưới dạng các phương thức trả về các tác vụ .NET (Task), chứ không phải là các đối tượng F # Async.
Sử dụng
Option
F #: Xem xét sử dụng các giá trị tùy chọn cho các kiểu trả về thay vì nâng cao các ngoại lệ (đối với mã F # -facing).
Cân nhắc sử dụng mẫu TryGetValue thay vì trả về giá trị tùy chọn F # (tùy chọn) trong các API .NET vani và thích nạp chồng phương thức hơn việc lấy các giá trị tùy chọn F # làm đối số.
Công đoàn phân biệt đối xử
F #: Sử dụng các liên hiệp phân biệt đối xử như một sự thay thế cho phân cấp lớp để tạo dữ liệu có cấu trúc cây
C #: không có hướng dẫn cụ thể cho việc này, nhưng khái niệm về công đoàn bị phân biệt đối xử là xa lạ với C #
Chức năng làm xoăn
F #: hàm cà ri là thành ngữ cho F #
C #: Không sử dụng các tham số trong các API .NET vani.
Kiểm tra giá trị null
F #: đây không phải là thành ngữ cho F #
C #: Xem xét kiểm tra các giá trị null trên các ranh giới API .NET vani.
Sử dụng F # loại
list
,map
,set
, vvF #: sử dụng chúng trong F # là thành ngữ
C #: Cân nhắc sử dụng các loại giao diện thu thập .NET IEnumerable và IDictionary cho các tham số và giá trị trả về trong các API .NET vani. ( Tức là không sử dụng F #
list
,map
,set
)
Các kiểu hàm (cái rõ ràng)
F #: việc sử dụng các hàm F # làm các giá trị là thành ngữ cho F #, rõ ràng
C #: Sử dụng kiểu ủy quyền .NET thay vì kiểu hàm F # trong các API .NET vani.
Tôi nghĩ những điều này đủ để chứng minh bản chất của câu hỏi của tôi.
Ngẫu nhiên, các hướng dẫn cũng có một phần câu trả lời:
... một chiến lược triển khai phổ biến khi phát triển các phương thức bậc cao hơn cho thư viện vanilla .NET là tác giả tất cả việc triển khai bằng cách sử dụng các kiểu hàm F #, sau đó tạo API công khai bằng cách sử dụng các đại biểu như một mặt tiền mỏng ở phía trên thực hiện F #.
Để tóm tắt.
Có một câu trả lời chắc chắn: không có thủ thuật biên dịch nào mà tôi đã bỏ qua .
Theo tài liệu hướng dẫn, có vẻ như việc tạo tác giả cho F # trước và sau đó tạo một trình bao bọc mặt tiền cho .NET là một chiến lược hợp lý.
Câu hỏi sau đó vẫn liên quan đến việc triển khai thực tế điều này:
Lắp ráp riêng biệt? hoặc là
Không gian tên khác nhau?
Nếu giải thích của tôi là đúng, Tomas gợi ý rằng việc sử dụng các không gian tên riêng biệt là đủ và phải là một giải pháp chấp nhận được.
Tôi nghĩ rằng tôi sẽ đồng ý với điều đó vì việc lựa chọn không gian tên sao cho nó không gây ngạc nhiên hoặc nhầm lẫn cho người dùng .NET / C #, điều đó có nghĩa là không gian tên cho họ có thể trông giống như nó là không gian tên chính cho họ. Người dùng F # sẽ phải chịu gánh nặng của việc chọn không gian tên cụ thể F #. Ví dụ:
FSharp.Foo.Bar -> không gian tên cho F # đối diện với thư viện
Foo.Bar -> không gian tên cho trình bao bọc .NET, thành ngữ cho C #